From 5d8e82793e571c8aa9ce7d4905b97119a8a3ee19 Mon Sep 17 00:00:00 2001 From: loucass003 Date: Thu, 16 Apr 2026 14:50:59 +0200 Subject: [PATCH] Steam --- gui/electron/main/cli.ts | 12 +- gui/electron/main/index.ts | 14 ++- gui/electron/main/paths.ts | 16 ++- gui/electron/preload/index.ts | 3 +- gui/electron/preload/interface.d.ts | 1 + gui/electron/shared.ts | 2 +- gui/public/i18n/en/translation.ftl | 5 + gui/src/App.tsx | 7 ++ gui/src/AppLayout.tsx | 11 +- gui/src/components/ErrorConsentModal.tsx | 60 ---------- gui/src/components/TopBar.tsx | 7 +- .../components/onboarding/UdevRulesModal.tsx | 113 ++++++++++++++++++ .../pages/ErrorCollectingConsent.tsx | 49 ++++++++ gui/src/hooks/config.ts | 2 + gui/src/utils/sentry.ts | 6 +- .../src/main/java/dev/slimevr/FeatureFlags.kt | 6 + .../src/main/java/dev/slimevr/VRServer.kt | 5 +- .../dev/slimevr/protocol/rpc/RPCHandler.kt | 2 + .../rpc/installinfo/RPCInstallInfoHandler.kt | 57 +++++++++ .../java/io/eiren/util/OperatingSystem.kt | 7 ++ .../src/main/java/dev/slimevr/desktop/Main.kt | 32 ++++- .../slimevr/desktop/games/vrchat/RegEdit.kt | 17 +++ .../desktop/install/drivers/InstallDrivers.kt | 20 ++++ .../desktop/install/drivers/InstallerUtils.kt | 16 +++ .../slimevr/desktop/install/drivers/Linux.kt | 60 ++++++++++ .../desktop/install/drivers/Windows.kt | 62 ++++++++++ 26 files changed, 510 insertions(+), 82 deletions(-) delete mode 100644 gui/src/components/ErrorConsentModal.tsx create mode 100644 gui/src/components/onboarding/UdevRulesModal.tsx create mode 100644 gui/src/components/onboarding/pages/ErrorCollectingConsent.tsx create mode 100644 server/core/src/main/java/dev/slimevr/FeatureFlags.kt create mode 100644 server/core/src/main/java/dev/slimevr/protocol/rpc/installinfo/RPCInstallInfoHandler.kt create mode 100644 server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/InstallDrivers.kt create mode 100644 server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/InstallerUtils.kt create mode 100644 server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/Linux.kt create mode 100644 server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/Windows.kt diff --git a/gui/electron/main/cli.ts b/gui/electron/main/cli.ts index 3171baee4f..4d706753e5 100644 --- a/gui/electron/main/cli.ts +++ b/gui/electron/main/cli.ts @@ -1,12 +1,20 @@ -import { program } from "commander"; +import { Option, program } from "commander"; program - .option('-p --path ', 'set launch path') + .option('-p, --path ', 'set launch path') + .option('-s, --steam', 'steam mode') + .option('-i, --install', 'run the driver installer') .option( '--skip-server-if-running', 'gui will not launch the server if it is already running' ) .allowUnknownOption(); +if (process.platform === "linux") { + const noUdevOption = new Option('--no-udev', 'disable udev warning'); + noUdevOption.negate = false; + program.addOption(noUdevOption) +} + program.parse(process.argv); export const options = program.opts(); diff --git a/gui/electron/main/index.ts b/gui/electron/main/index.ts index 2af187d184..812cba242d 100644 --- a/gui/electron/main/index.ts +++ b/gui/electron/main/index.ts @@ -20,6 +20,7 @@ import { getPlatform, handleIpc, isPortAvailable } from './utils'; import { findServerJar, findSystemJRE, + getExeFolder, getGuiDataFolder, getLogsFolder, getServerDataFolder, @@ -182,6 +183,8 @@ handleIpc(IPC_CHANNELS.GET_FOLDER, (e, folder) => { return getGuiDataFolder(); case 'logs': return getLogsFolder(); + case 'exe': + return getExeFolder(); } }); @@ -394,7 +397,16 @@ const spawnServer = async () => { logger.info({ javaBin, serverJar }, 'Found Java and server jar'); const platform = getPlatform(); const serverWorkdir = getServerDataFolder() - const serverProcess = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run'], { + + const serverArgs = ['-Xmx128M', '-jar', serverJar] + if (options.steam) serverArgs.push(`--steam`) + if (options.install) serverArgs.push(`--install`) + if (options.noUdev) serverArgs.push(`--no-udev`) + + serverArgs.push('run') + + + const serverProcess = spawn(javaBin, serverArgs, { cwd: serverWorkdir, shell: false, env: diff --git a/gui/electron/main/paths.ts b/gui/electron/main/paths.ts index da9dfb97ce..065f5cada1 100644 --- a/gui/electron/main/paths.ts +++ b/gui/electron/main/paths.ts @@ -48,12 +48,21 @@ export const getLogsFolder = () => { return join(getGuiDataFolder(), 'logs'); }; +export const getExeFolder = () => { + return path.dirname(app.getPath('exe')); +}; + export const getWindowStateFile = () => join(getServerDataFolder(), '.window-state.json'); const localJavaBin = (sharedDir: string) => { - const jre = join(sharedDir, 'jre/bin', javaBin); - return jre; + const platform = getPlatform(); + switch (platform) { + case 'macos': + return join(sharedDir, '../../../../jre/Contents/Home/bin', javaBin); + default: + return join(sharedDir, 'jre/bin', javaBin); + } }; const javaHomeBin = () => { @@ -112,6 +121,9 @@ export const findServerJar = () => { // For flatpack container path.resolve('/app/share/slimevr/'), path.resolve('/usr/share/slimevr/'), + + // For macos on steam + path.resolve(`${app.getPath('exe')}/../../../../`), ]; return paths .filter((p) => !!p) diff --git a/gui/electron/preload/index.ts b/gui/electron/preload/index.ts index 376393c94d..43e0614a5c 100644 --- a/gui/electron/preload/index.ts +++ b/gui/electron/preload/index.ts @@ -36,5 +36,6 @@ contextBridge.exposeInMainWorld('electronAPI', { openLogsFolder: async () => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, await ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'logs')), openFile: (path) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, path), ghGet: (req) => ipcRenderer.invoke(IPC_CHANNELS.GH_FETCH, req), - setPresence: (options) => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_PRESENCE, options) + setPresence: (options) => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_PRESENCE, options), + getInstallDir: () => ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'exe') } satisfies IElectronAPI); diff --git a/gui/electron/preload/interface.d.ts b/gui/electron/preload/interface.d.ts index e7822f9a56..d75df5cc7f 100644 --- a/gui/electron/preload/interface.d.ts +++ b/gui/electron/preload/interface.d.ts @@ -56,6 +56,7 @@ export interface IElectronAPI { openFile: (path: string) => void; ghGet: (options: T) => Promise; setPresence: (options: DiscordPresence) => void; + getInstallDir: () => Promise; } declare global { diff --git a/gui/electron/shared.ts b/gui/electron/shared.ts index 5abe218762..96c74dc8d6 100644 --- a/gui/electron/shared.ts +++ b/gui/electron/shared.ts @@ -41,7 +41,7 @@ export interface IpcInvokeMap { value?: unknown; }) => Promise; [IPC_CHANNELS.OPEN_FILE]: (path: string) => void; - [IPC_CHANNELS.GET_FOLDER]: (folder: 'config' | 'logs') => string; + [IPC_CHANNELS.GET_FOLDER]: (folder: 'config' | 'logs' | 'exe') => string; [IPC_CHANNELS.GH_FETCH]: ( options: T ) => Promise; diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 270ea396d0..e91247bac8 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -1024,6 +1024,11 @@ onboarding-reset_tutorial-2 = Tap the highlighted tracker { $taps } times to tri You need to be in a pose like you are skiing as shown in the Automatic Mounting wizard, and you have a 3 second delay (configurable) before it gets triggered. +## Install info +install-info_udev-rules_modal_title = Hardware udev access rules not found +install-info_udev-rules_warning = Access rules via udev are required for serial console access & dongle connection. Paste the following command into your terminal to add the udev rules. +install-info_udev-rules_modal_button = Close +install-info_udev-rules_modal-dont-show-again_checkbox = Don't show again ## Setup start onboarding-home = Welcome to SlimeVR onboarding-home-start = Let's get set up! diff --git a/gui/src/App.tsx b/gui/src/App.tsx index c6d3417525..9674789d43 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -17,6 +17,7 @@ import { AutomaticProportionsPage } from './components/onboarding/pages/body-pro import { ManualProportionsPage } from './components/onboarding/pages/body-proportions/ManualProportions'; import { ConnectTrackersPage } from './components/onboarding/pages/ConnectTracker'; import { HomePage } from './components/onboarding/pages/Home'; +import { ErrorCollectingConsentPage } from './components/onboarding/pages/ErrorCollectingConsent'; import { AutomaticMountingPage } from './components/onboarding/pages/mounting/AutomaticMounting'; import { ManualMountingPage } from './components/onboarding/pages/mounting/ManualMounting'; import { TrackersAssignPage } from './components/onboarding/pages/trackers-assign/TrackerAssignment'; @@ -58,6 +59,7 @@ import { QuizMocapPosQuestion } from './components/onboarding/pages/quiz/MocapPr import { ElectronContextC, provideElectron } from './hooks/electron'; import { AppLocalizationProvider } from './i18n/config'; import { openUrl } from './hooks/crossplatform'; +import { UdevRulesModal } from './components/onboarding/UdevRulesModal'; export const GH_REPO = 'SlimeVR/SlimeVR-Server'; export const VersionContext = createContext(''); @@ -75,6 +77,7 @@ function Layout() { + }> } /> + } + /> } /> } /> } /> diff --git a/gui/src/AppLayout.tsx b/gui/src/AppLayout.tsx index 6e0aee1c2f..ab067c3a97 100644 --- a/gui/src/AppLayout.tsx +++ b/gui/src/AppLayout.tsx @@ -1,9 +1,10 @@ import { useLayoutEffect } from 'react'; import { useConfig } from './hooks/config'; -import { Outlet, useNavigate } from 'react-router-dom'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; export function AppLayout() { const { config } = useConfig(); + const { pathname } = useLocation(); const navigate = useNavigate(); useLayoutEffect(() => { @@ -28,10 +29,14 @@ export function AppLayout() { }, [config]); useLayoutEffect(() => { - if (config && !config.doneOnboarding) { + if ( + config && + !config.doneOnboarding && + !pathname.startsWith('/onboarding/') + ) { navigate('/onboarding/home'); } - }, [config?.doneOnboarding]); + }, [config]); return ( <> diff --git a/gui/src/components/ErrorConsentModal.tsx b/gui/src/components/ErrorConsentModal.tsx deleted file mode 100644 index 358a80e16b..0000000000 --- a/gui/src/components/ErrorConsentModal.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Localized, useLocalization } from '@fluent/react'; -import { BaseModal } from './commons/BaseModal'; -import { Button } from './commons/Button'; -import { Typography } from './commons/Typography'; - -export function ErrorConsentModal({ - isOpen = true, - cancel, - accept, -}: { - /** - * Is the parent/sibling component opened? - */ - isOpen: boolean; - /** - * Function to trigger when you still want to close the app - */ - accept: () => void; - /** - * Function to trigger when cancelling app close - */ - cancel?: () => void; -}) { - const { l10n } = useLocalization(); - - return ( - -
- <> -
-
- - {l10n.getString('error_collection_modal-title')} - - , - h1: , - }} - > - - -
-
- - - - -
-
- ); -} diff --git a/gui/src/components/TopBar.tsx b/gui/src/components/TopBar.tsx index 60d7b2d6ed..9b662e1b50 100644 --- a/gui/src/components/TopBar.tsx +++ b/gui/src/components/TopBar.tsx @@ -22,7 +22,6 @@ import { GearIcon } from './commons/icon/GearIcon'; import { TrackersStillOnModal } from './TrackersStillOnModal'; import { useConfig } from '@/hooks/config'; import { TrayOrExitModal } from './TrayOrExitModal'; -import { ErrorConsentModal } from './ErrorConsentModal'; import { useAtomValue } from 'jotai'; import { connectedIMUTrackersAtom } from '@/store/app-store'; import { useElectron } from '@/hooks/electron'; @@ -72,6 +71,7 @@ export function TopBar({ await saveConfig(); electron.api.close(); }; + const tryCloseApp = async (dontTray = false) => { if (!electron.isElectron) throw 'no electron'; @@ -286,11 +286,6 @@ export function TopBar({ setConnectedTrackerWarning(false); }} /> - setConfig({ errorTracking: true })} - cancel={() => setConfig({ errorTracking: false })} - /> ); } diff --git a/gui/src/components/onboarding/UdevRulesModal.tsx b/gui/src/components/onboarding/UdevRulesModal.tsx new file mode 100644 index 0000000000..b89af1e733 --- /dev/null +++ b/gui/src/components/onboarding/UdevRulesModal.tsx @@ -0,0 +1,113 @@ +import { useState, useEffect } from 'react'; +import { Button } from '@/components/commons/Button'; +import { BaseModal } from '@/components/commons/BaseModal'; +import { CheckboxInternal } from '@/components/commons/Checkbox'; +import { Typography } from '@/components/commons/Typography'; +import { useElectron } from '@/hooks/electron'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { RpcMessage, InstalledInfoResponseT } from 'solarxr-protocol'; +import { useConfig } from '@/hooks/config'; +import { useLocalization } from '@fluent/react'; + +export function UdevRulesModal() { + const { config, setConfig } = useConfig(); + const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); + const electron = useElectron(); + const [udevContent, setUdevContent] = useState(''); + const [isUdevInstalledResponse, setIsUdevInstalledResponse] = useState(true); + const [showUdevWarning, setShowUdevWarning] = useState(false); + const [dontShowThisSession, setDontShowThisSession] = useState(false); + const [dontShowAgain, setDontShowAgain] = useState(false); + const { l10n } = useLocalization(); + + const handleUdevContent = async () => { + if (electron.isElectron) { + const dir = await electron.api.getInstallDir(); + const rulesPath = `${dir}/69-slimevr-devices.rules`; + setUdevContent( + `cat ${rulesPath} | sudo sh -c 'tee /etc/udev/rules.d/69-slimevr-devices.rules >/dev/null && udevadm control --reload-rules && udevadm trigger'` + ); + } + }; + + useEffect(() => { + handleUdevContent(); + }, []); + + useEffect(() => { + if (!config) throw 'Invalid state!'; + if (electron.isElectron) { + const isLinux = electron.data().os.type === 'linux'; + const udevMissing = !isUdevInstalledResponse; + const notHiddenGlobally = !config.dontShowUdevModal; + const notHiddenThisSession = !dontShowThisSession; + const shouldShow = + isLinux && udevMissing && notHiddenGlobally && notHiddenThisSession; + setShowUdevWarning(shouldShow); + } + }, [config, isUdevInstalledResponse, dontShowThisSession]); + + useEffect(() => { + sendRPCPacket( + RpcMessage.InstalledInfoRequest, + new InstalledInfoResponseT() + ); + }, []); + + useRPCPacket( + RpcMessage.InstalledInfoResponse, + ({ isUdevInstalled }: InstalledInfoResponseT) => { + setIsUdevInstalledResponse(isUdevInstalled); + } + ); + + const handleModalClose = () => { + if (!config) throw 'Invalid State!'; + setConfig({ dontShowUdevModal: dontShowAgain }); + setDontShowThisSession(true); + }; + + const copyToClipboard = () => { + navigator.clipboard.writeText(udevContent); + }; + + return ( + +
+
+
+ + +
+
+
+ +
+
+
{udevContent}
+
+
+
+
+ setDontShowAgain(e.currentTarget.checked)} + /> +
+
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/ErrorCollectingConsent.tsx b/gui/src/components/onboarding/pages/ErrorCollectingConsent.tsx new file mode 100644 index 0000000000..5be6404e55 --- /dev/null +++ b/gui/src/components/onboarding/pages/ErrorCollectingConsent.tsx @@ -0,0 +1,49 @@ +import { Localized } from '@fluent/react'; +import { Typography } from '@/components/commons/Typography'; +import { Button } from '@/components/commons/Button'; +import { useConfig } from '@/hooks/config'; + +export function ErrorCollectingConsentPage() { + const { setConfig } = useConfig(); + + const accept = () => { + setConfig({ errorTracking: true }); + }; + + const cancel = () => { + setConfig({ errorTracking: false }); + }; + + return ( +
+
+
+ + , + h1: , + }} + > + + +
+
+
+
+
+ ); +} diff --git a/gui/src/hooks/config.ts b/gui/src/hooks/config.ts index 03e0de6b7e..39cc7cfabf 100644 --- a/gui/src/hooks/config.ts +++ b/gui/src/hooks/config.ts @@ -48,6 +48,7 @@ export interface Config { homeLayout: 'default' | 'table'; skeletonPreview: boolean; lastUsedProportions: 'manual' | 'autobone' | 'scaled' | null; + dontShowUdevModal: boolean; } export interface ConfigContext { @@ -79,6 +80,7 @@ export const defaultConfig: Config = { homeLayout: 'default', skeletonPreview: true, lastUsedProportions: null, + dontShowUdevModal: false, }; const localStore: CrossStorage = { diff --git a/gui/src/utils/sentry.ts b/gui/src/utils/sentry.ts index 087d3f1e0f..215119434c 100644 --- a/gui/src/utils/sentry.ts +++ b/gui/src/utils/sentry.ts @@ -11,8 +11,12 @@ import { DeviceDataT } from 'solarxr-protocol'; export function getSentryOrCompute(enabled = false, uuid: string) { Sentry.setUser({ id: uuid }); + // if sentry is already initialized - SKIP - if (enabled && Sentry.isInitialized()) return; + if (enabled && Sentry.isInitialized()) { + log('Sentry already enabled, skipping initialization'); + return; + } const client = Sentry.getClient(); if (client) { diff --git a/server/core/src/main/java/dev/slimevr/FeatureFlags.kt b/server/core/src/main/java/dev/slimevr/FeatureFlags.kt new file mode 100644 index 0000000000..e09f150675 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/FeatureFlags.kt @@ -0,0 +1,6 @@ +package dev.slimevr + +data class FeatureFlags( + var steam: Boolean = false, + var skipCheckUdev: Boolean = false, +) diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index b8c839b6d4..074631cc6e 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -57,6 +57,7 @@ const val SLIMEVR_IDENTIFIER = "dev.slimevr.SlimeVR" class VRServer @JvmOverloads constructor( bridgeProvider: BridgeProvider = { _, _ -> sequence {} }, + featureFlagsProvider: (VRServer) -> FeatureFlags = { _ -> FeatureFlags() }, serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() }, flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null }, vrcConfigHandlerProvider: (VRServer) -> VRCConfigHandler = { _ -> VRCConfigHandlerStub() }, @@ -84,6 +85,9 @@ class VRServer @JvmOverloads constructor( @JvmField val deviceManager: DeviceManager + // UwU + val featureFlags: FeatureFlags = featureFlagsProvider(this) + @JvmField val bvhRecorder: BVHRecorder @@ -127,7 +131,6 @@ class VRServer @JvmOverloads constructor( val serverGuards = ServerGuards() init { - // UwU deviceManager = DeviceManager(this) serialHandler = serialHandlerProvider(this) serialFlashingHandler = flashingHandlerProvider(this) diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt index a0d9a78c96..57be55772a 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt @@ -10,6 +10,7 @@ import dev.slimevr.protocol.datafeed.createTrackerId import dev.slimevr.protocol.rpc.autobone.RPCAutoBoneHandler import dev.slimevr.protocol.rpc.firmware.RPCFirmwareUpdateHandler import dev.slimevr.protocol.rpc.games.vrchat.RPCVRChatHandler +import dev.slimevr.protocol.rpc.installinfo.RPCInstallInfoHandler import dev.slimevr.protocol.rpc.reset.RPCResetHandler import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler import dev.slimevr.protocol.rpc.serial.RPCSerialHandler @@ -52,6 +53,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler) { System.setProperty("awt.useSystemAAFontSettings", "on") System.setProperty("swing.aatext", "true") @@ -50,23 +54,38 @@ fun main(args: Array) { val parser: CommandLineParser = DefaultParser() val formatter = HelpFormatter() val options = Options() + val isLinux = OperatingSystem.currentPlatform == OperatingSystem.LINUX options.addOption("h", "help", false, "Show help") options.addOption("V", "version", false, "Show version") + options.addOption("i", "install", true, "Run the driver install") + options.addOption("s", "steam", true, "Run the server in steam mode") + if (isLinux) { + options.addOption("u", "no-udev", false, "Skip checking if udev rules are installed") + } + val cmd: CommandLine = try { parser.parse(options, args, true) } catch (e: org.apache.commons.cli.ParseException) { formatter.printHelp("slimevr.jar", options) exitProcess(1) } - if (cmd.hasOption("help")) { formatter.printHelp("slimevr.jar", options) exitProcess(0) } if (cmd.hasOption("version")) { - println("SlimeVR Server $VERSION") + LogManager.info("SlimeVR Server $VERSION") + exitProcess(0) + } + if (cmd.hasOption("install")) { + val installDrivers = InstallDrivers() + installDrivers.runInstaller() exitProcess(0) } + if (cmd.hasOption("steam")) { + featureFlags.steam = true + } + featureFlags.skipCheckUdev = !isLinux || cmd.hasOption("no-udev") if (cmd.args.isEmpty()) { System.err.println("No command specified, expected 'run'") @@ -99,6 +118,12 @@ fun main(args: Array) { return } + val isInstallDisabled = System.getenv("SLIME_SERVER_DISABLE_INSTALLER")?.toInt() + if (featureFlags.steam && isInstallDisabled != 1) { + val installDrivers = InstallDrivers() + installDrivers.runInstaller() + } + val configDir = resolveConfig() LogManager.info("Using config dir: $configDir") @@ -126,6 +151,7 @@ fun main(args: Array) { try { val vrServer = VRServer( ::provideBridges, + { _ -> featureFlags }, { _ -> DesktopSerialHandler() }, { _ -> DesktopSerialFlashingHandler() }, { _ -> DesktopVRCConfigHandler() }, @@ -133,7 +159,6 @@ fun main(args: Array) { configManager = configManager, ) vrServer.start() - // Start service for USB HID trackers DesktopHIDManager( "Sensors HID service", @@ -218,7 +243,6 @@ fun provideBridges( ) yield(linuxBridge) } - yield( UnixSocketBridge( server, diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/RegEdit.kt b/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/RegEdit.kt index b0f37bad9a..dcbc7a99fc 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/RegEdit.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/games/vrchat/RegEdit.kt @@ -20,6 +20,7 @@ abstract class AbstractRegEdit { abstract fun getQwordValue(path: String, key: String): Double? abstract fun getDwordValue(path: String, key: String): Int? abstract fun getVRChatKeys(path: String): Map + abstract fun getKeyByPath(hkey: WinReg.HKEY, path: String): Map } class RegEditWindows : AbstractRegEdit() { @@ -75,6 +76,19 @@ class RegEditWindows : AbstractRegEdit() { } return keysMap } + + override fun getKeyByPath(hkey: WinReg.HKEY, path: String): Map { + val keysMap = mutableMapOf() + + try { + Advapi32Util.registryGetValues(hkey, path).forEach { + keysMap[it.key.replace("""_h\d+$""".toRegex(), "")] = it.value.toString() + } + } catch (e: Exception) { + LogManager.severe("[RegEdit] Error reading values from registry", e) + } + return keysMap + } } class RegEditLinux : AbstractRegEdit() { @@ -142,6 +156,9 @@ class RegEditLinux : AbstractRegEdit() { return keysMap } + // This function should never run on Linux. + override fun getKeyByPath(hkey: WinReg.HKEY, path: String): Map = mutableMapOf() + companion object { private fun findAppLibraryLocation(steamPath: Path, appId: Int): Path? { val keyValueRegex = Regex(""""(\w+)"[ \t]*(?:"(.+)")?""") diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/InstallDrivers.kt b/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/InstallDrivers.kt new file mode 100644 index 0000000000..c876f262ca --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/InstallDrivers.kt @@ -0,0 +1,20 @@ +package dev.slimevr.desktop.install.drivers + +import io.eiren.util.logging.LogManager + +class InstallDrivers { + + val os = System.getProperty("os.name").lowercase() + + fun runInstaller() { + if (os.contains("linux")) { + val linuxUpdater = Linux() + linuxUpdater.updateLinux() + } else if (os.contains("windows")) { + val windowsUpdater = Windows() + windowsUpdater.updateWindows() + } else { + LogManager.warning("Updater doesn't support operating system '$os'") + } + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/InstallerUtils.kt b/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/InstallerUtils.kt new file mode 100644 index 0000000000..3aae4d6215 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/InstallerUtils.kt @@ -0,0 +1,16 @@ +package dev.slimevr.desktop.install.drivers + +import io.eiren.util.logging.LogManager +import java.io.IOException + +fun executeShellCommand(vararg command: String): String? = try { + val process = ProcessBuilder(*command) + .redirectErrorStream(true) + .start() + process.inputStream.bufferedReader().readText().also { + process.waitFor() + } +} catch (e: IOException) { + LogManager.warning("Error executing shell command", e) + null +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/Linux.kt b/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/Linux.kt new file mode 100644 index 0000000000..e6db3da2e0 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/Linux.kt @@ -0,0 +1,60 @@ +package dev.slimevr.desktop.install.drivers + +import dev.slimevr.desktop.featureFlags +import io.eiren.util.logging.LogManager + +class Linux { + + val path: String = System.getProperty("user.dir") + + fun updateLinux() { + updateLinuxSteamVRDriver() + feeder() + } + + fun updateLinuxSteamVRDriver() { + val pathRegPath = "${System.getProperty("user.home")}/.steam/steam/steamapps/common/SteamVR/bin/vrpathreg.sh" + val vrPathRegContents = executeShellCommand(pathRegPath) + if (vrPathRegContents == null) { + LogManager.warning("SteamVR driver installation failed") + return + } + if (vrPathRegContents.contains("slimevr")) { + LogManager.info("SteamVR driver is already installed") + return + } + + executeShellCommand(pathRegPath, "adddriver", "$path/$LINUX_STEAM_DRIVER_DIRECTORY") + + if (executeShellCommand(pathRegPath)?.contains("slimevr") != true) { + LogManager.warning("Failed to install SteamVR driver") + return + } + LogManager.info("SteamVR driver successfully installed") + } + + fun feeder() { + executeShellCommand("chmod", "+x", "$path/$LINUX_FEEDER_DIRECTORY/SlimeVR-Feeder-App") + + val command = if (featureFlags.steam) { + arrayOf("steam-runtime-launch-client", "--alongside-steam", "--", "$path/$LINUX_FEEDER_DIRECTORY/SlimeVR-Feeder-App", "--install") + } else { + arrayOf("$path/$LINUX_FEEDER_DIRECTORY/SlimeVR-Feeder-App", "--install") + } + val feederOutput = executeShellCommand(*command) + if (feederOutput == null) { + LogManager.warning("Error installing feeder") + return + } + if (feederOutput.lowercase().contains("manifest is not installed")) { + LogManager.warning("Could not install feeder application") + } else { + LogManager.info("Successfully installed feeder application") + } + } + + companion object { + private const val LINUX_STEAM_DRIVER_DIRECTORY = "slimevr-openvr-driver-x64-linux" + private const val LINUX_FEEDER_DIRECTORY = "SlimeVR-Feeder-App-Linux" + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/Windows.kt b/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/Windows.kt new file mode 100644 index 0000000000..fbdc894ed4 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/install/drivers/Windows.kt @@ -0,0 +1,62 @@ +package dev.slimevr.desktop.install.drivers + +import com.sun.jna.platform.win32.WinReg +import dev.slimevr.desktop.games.vrchat.RegEditWindows +import io.eiren.util.logging.LogManager + +class Windows { + + val path: String = System.getProperty("user.dir") + + fun updateWindows() { + steamVRDriver() + feeder() + } + + fun feeder() { + val feederOutput = executeShellCommand("${path}\\${WINDOWS_FEEDER_DIRECTORY}\\SlimeVR-Feeder-App.exe", "--install") + if (feederOutput == null) { + LogManager.warning("Error installing feeder") + return + } + if (feederOutput.lowercase().contains("manifest is not installed")) { + LogManager.warning("Could not install feeder application") + } else { + LogManager.info("Successfully installed feeder application") + } + } + + fun steamVRDriver() { + val regEdit = RegEditWindows() + val regQuery = regEdit.getKeyByPath(WinReg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App 250820") + val steamVRLocation = regQuery["InstallLocation"] + if (steamVRLocation == null || !steamVRLocation.contains("SteamVR")) { + LogManager.warning("Can't find SteamVR, unable to install SteamVR driver") + return + } + + val pathRegPath = "${steamVRLocation}\\bin\\win64\\vrpathreg.exe" + val vrPathRegContents = executeShellCommand(pathRegPath, "finddriver", "slimevr") + if (vrPathRegContents == null) { + LogManager.warning("Error installing SteamVR driver") + return + } + if (vrPathRegContents.contains("slimevr")) { + LogManager.info("SteamVR driver is already installed") + return + } + + executeShellCommand(pathRegPath, "adddriver", "${path}\\${WINDOWS_STEAMVR_DRIVER_DIRECTORY}") + + if (executeShellCommand(pathRegPath, "finddriver", "slimevr")?.contains("slimevr") != true) { + LogManager.warning("Failed to install SlimeVR driver") + return + } + LogManager.info("SteamVR driver successfully installed") + } + + companion object { + private const val WINDOWS_STEAMVR_DRIVER_DIRECTORY = "slimevr-openvr-driver-win64" + private const val WINDOWS_FEEDER_DIRECTORY = "SlimeVR-Feeder-App-win64" + } +}