Merge remote-tracking branch 'origin/hannah/steam' into hannah/keybinds

This commit is contained in:
HannahPadd
2026-03-20 14:10:46 +01:00
28 changed files with 505 additions and 84 deletions

View File

@@ -1,12 +1,20 @@
import { program } from "commander";
import { Option, program } from "commander";
program
.option('-p --path <path>', 'set launch path')
.option('-p, --path <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();

View File

@@ -20,6 +20,7 @@ import { getPlatform, handleIpc, isPortAvailable } from './utils';
import {
findServerJar,
findSystemJRE,
getExeFolder,
getGuiDataFolder,
getLogsFolder,
getServerDataFolder,
@@ -177,6 +178,8 @@ handleIpc(IPC_CHANNELS.GET_FOLDER, (e, folder) => {
return getGuiDataFolder();
case 'logs':
return getLogsFolder();
case 'exe':
return getExeFolder();
}
});
@@ -385,8 +388,17 @@ const spawnServer = async () => {
}
logger.info({ javaBin, serverJar }, 'Found Java and server jar');
const serverArgs = ['-Xmx128M', '-jar', serverJar]
const process = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run']);
if (options.steam) serverArgs.push(`--steam`)
if (options.install) serverArgs.push(`--install`)
if (options.noUdev) serverArgs.push(`--no-udev`)
serverArgs.push('run')
const process = spawn(javaBin, serverArgs)
logger.info(`Java start command: ${serverArgs.join(' ')})`)
process.stdout?.on('data', (message) => {
mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, {

View File

@@ -48,6 +48,10 @@ export const getLogsFolder = () => {
return join(getGuiDataFolder(), 'logs');
};
export const getExeFolder = () => {
return path.dirname(app.getPath('exe'));
}
export const getWindowStateFile = () =>
join(getServerDataFolder(), '.window-state.json');

View File

@@ -35,5 +35,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);

View File

@@ -55,6 +55,7 @@ export interface IElectronAPI {
openFile: (path: string) => void;
ghGet: <T extends GHGet>(options: T) => Promise<GHReturn[T['type']]>;
setPresence: (options: DiscordPresence) => void;
getInstallDir: () => Promise<string>;
}
declare global {

View File

@@ -41,7 +41,7 @@ export interface IpcInvokeMap {
value?: unknown;
}) => Promise<unknown>;
[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]: <T extends GHGet>(
options: T
) => Promise<GHReturn[T['type']]>;

View File

@@ -985,6 +985,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!

View File

@@ -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';
@@ -54,6 +55,7 @@ import { KeybindSettings } from './components/settings/pages/KeybindSettings';
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('');
@@ -71,6 +73,7 @@ function Layout() {
<SerialDetectionModal />
<VersionUpdateModal />
<UnknownDeviceModal />
<UdevRulesModal />
<SentryRoutes>
<Route element={<AppLayout />}>
<Route
@@ -149,6 +152,10 @@ function Layout() {
}
>
<Route path="home" element={<HomePage />} />
<Route
path="error-collecting-consent"
element={<ErrorCollectingConsentPage />}
/>
<Route path="wifi-creds" element={<WifiCredsPage />} />
<Route path="connect-trackers" element={<ConnectTrackersPage />} />
<Route path="trackers-assign" element={<TrackersAssignPage />} />

View File

@@ -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 (
<>

View File

@@ -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 (
<BaseModal isOpen={isOpen} onRequestClose={cancel} closeable={false}>
<div className="flex flex-col gap-3">
<>
<div className="flex flex-col items-center gap-3 fill-accent-background-20">
<div className="flex flex-col items-center gap-2 max-w-[512px]">
<Typography variant="main-title">
{l10n.getString('error_collection_modal-title')}
</Typography>
<Localized
id={'error_collection_modal-description_v2'}
elems={{
b: <b />,
h1: <span className="text-lg font-bold" />,
}}
>
<Typography
variant="standard"
whitespace="whitespace-pre-line"
/>
</Localized>
</div>
</div>
<Button variant="primary" onClick={accept}>
{l10n.getString('error_collection_modal-confirm')}
</Button>
<Button variant="tertiary" onClick={cancel}>
{l10n.getString('error_collection_modal-cancel')}
</Button>
</>
</div>
</BaseModal>
);
}

View File

@@ -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);
}}
/>
<ErrorConsentModal
isOpen={config?.errorTracking === null}
accept={() => setConfig({ errorTracking: true })}
cancel={() => setConfig({ errorTracking: false })}
/>
</>
);
}

View File

@@ -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 (
<BaseModal isOpen={showUdevWarning} appendClasses={'w-full max-w-2xl'}>
<div className="flex w-full h-full flex-col gap-4">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<Typography
variant="main-title"
id="install-info_udev-rules_modal_title"
/>
<Typography id="install-info_udev-rules_warning" />
</div>
<div className="relative w-full max-w-2xl">
<div className="absolute right-2 top-2">
<Button variant="secondary" onClick={copyToClipboard}>
Copy
</Button>
</div>
<div className="bg-background-80 rounded-lg overflow-auto p-2 w-full h-[300px]">
<pre className="text-wrap">{udevContent}</pre>
</div>
</div>
</div>
<div className="flex justify-between gap-2">
<CheckboxInternal
label={l10n.getString(
'install-info_udev-rules_modal-dont-show-again_checkbox'
)}
name="dismiss-udev-rules-checkbox"
onChange={(e) => setDontShowAgain(e.currentTarget.checked)}
/>
<Button
variant="primary"
onClick={handleModalClose}
id="install-info_udev-rules_modal_button"
/>
</div>
</div>
</BaseModal>
);
}

View File

@@ -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 (
<div className="flex items-center justify-center h-full flex-col gap-3 p-4">
<div className="max-w-2xl flex flex-col gap-4">
<div className="flex flex-col w-full gap-4">
<Typography variant="main-title" id="error_collection_modal-title" />
<Localized
id="error_collection_modal-description_v2"
elems={{
b: <b />,
h1: <span className="text-md font-bold" />,
}}
>
<Typography variant="standard" whitespace="whitespace-pre-line" />
</Localized>
</div>
<div className="flex flex-row gap-2 justify-between">
<Button
variant="tertiary"
to="/onboarding/wifi-creds"
onClick={cancel}
id="error_collection_modal-cancel"
/>
<Button
variant="primary"
to="/onboarding/wifi-creds"
onClick={accept}
id="error_collection_modal-confirm"
/>
</div>
</div>
</div>
);
}

View File

@@ -19,9 +19,11 @@ export function HomePage() {
<Typography variant="mobile-title">
{l10n.getString('onboarding-home')}
</Typography>
<Button variant="primary" to="/onboarding/wifi-creds">
{l10n.getString('onboarding-home-start')}
</Button>
<Button
variant="primary"
id="onboarding-home-start"
to="/onboarding/error-collecting-consent"
/>
</div>
<div className="absolute right-4 bottom-4 z-50">
<LangSelector />

View File

@@ -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 = {

View File

@@ -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) {

View File

@@ -0,0 +1,6 @@
package dev.slimevr
data class FeatureFlags(
var steam: Boolean = false,
var skipCheckUdev: Boolean = false,
)

View File

@@ -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
@@ -130,7 +134,6 @@ class VRServer @JvmOverloads constructor(
init {
// UwU
deviceManager = DeviceManager(this)
serialHandler = serialHandlerProvider(this)
serialFlashingHandler = flashingHandlerProvider(this)

View File

@@ -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.keybinds.RPCKeybindHandler
import dev.slimevr.protocol.rpc.reset.RPCResetHandler
import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler

View File

@@ -0,0 +1,57 @@
package dev.slimevr.protocol.rpc.installinfo
import com.google.flatbuffers.FlatBufferBuilder
import dev.slimevr.protocol.GenericConnection
import dev.slimevr.protocol.ProtocolAPI
import dev.slimevr.protocol.rpc.RPCHandler
import io.eiren.util.logging.LogManager
import solarxr_protocol.rpc.InstalledInfoResponse.createInstalledInfoResponse
import solarxr_protocol.rpc.RpcMessage
import solarxr_protocol.rpc.RpcMessageHeader
import java.io.IOException
class RPCInstallInfoHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
init {
rpcHandler.registerPacketListener(RpcMessage.InstalledInfoRequest, ::onInstalledInfoRequest)
}
fun onInstalledInfoRequest(conn: GenericConnection, messageHeader: RpcMessageHeader?) {
if (api.server.featureFlags.skipCheckUdev) {
return
}
val command = if (api.server.featureFlags.steam) {
arrayOf("steam-runtime-launch-client", "--alongside-steam", "--", "udevadm", "cat")
} else {
arrayOf("udevadm", "cat")
}
val udevResponse = executeShellCommand(*command)
if (udevResponse == null) {
LogManager.warning("Server couldn't verify if udev is installed")
return
}
val response = udevResponse.contains("slime")
val fbb = FlatBufferBuilder(1024)
val outbound = this.rpcHandler.createRPCMessage(
fbb,
RpcMessage.InstalledInfoResponse,
createInstalledInfoResponse(fbb, response),
)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
}
private 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
}

View File

@@ -1,5 +1,6 @@
package io.eiren.util
import dev.slimevr.SLIMEVR_IDENTIFIER
import java.io.File
import java.nio.file.Path
import java.util.*
@@ -38,6 +39,12 @@ enum class OperatingSystem(
var dir = System.getenv("SLIMEVR_SOCKET_DIR")
if (dir != null) return dir
if (currentPlatform == LINUX) {
val isPressureVessel = System.getenv("PRESSURE_VESSEL_RUNTIME")?.isNotEmpty() == true
if (isPressureVessel) {
dir = System.getenv("XDG_CONFIG_HOME")?.let { Path(it, SLIMEVR_IDENTIFIER).toString() }
?: System.getenv("HOME")?.let { Path(it, ".local", "share", SLIMEVR_IDENTIFIER).toString() }
if (dir != null) return dir
}
dir = System.getenv("XDG_RUNTIME_DIR")
if (dir != null) return dir
}

View File

@@ -2,6 +2,7 @@
package dev.slimevr.desktop
import dev.slimevr.FeatureFlags
import dev.slimevr.Keybinding
import dev.slimevr.SLIMEVR_IDENTIFIER
import dev.slimevr.VRServer
@@ -9,6 +10,7 @@ import dev.slimevr.bridge.Bridge
import dev.slimevr.config.ConfigManager
import dev.slimevr.desktop.firmware.DesktopSerialFlashingHandler
import dev.slimevr.desktop.games.vrchat.DesktopVRCConfigHandler
import dev.slimevr.desktop.install.drivers.InstallDrivers
import dev.slimevr.desktop.platform.SteamVRBridge
import dev.slimevr.desktop.platform.linux.UnixSocketBridge
import dev.slimevr.desktop.platform.linux.UnixSocketRpcBridge
@@ -43,6 +45,8 @@ val VERSION =
(GIT_VERSION_TAG.ifEmpty { GIT_COMMIT_HASH }) +
if (GIT_CLEAN) "" else "-dirty"
val featureFlags = FeatureFlags()
fun main(args: Array<String>) {
System.setProperty("awt.useSystemAAFontSettings", "on")
System.setProperty("swing.aatext", "true")
@@ -50,23 +54,38 @@ fun main(args: Array<String>) {
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<String>) {
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<String>) {
try {
val vrServer = VRServer(
::provideBridges,
{ _ -> featureFlags },
{ _ -> DesktopSerialHandler() },
{ _ -> DesktopSerialFlashingHandler() },
{ _ -> DesktopVRCConfigHandler() },
@@ -133,7 +159,6 @@ fun main(args: Array<String>) {
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,

View File

@@ -19,6 +19,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<String, String>
abstract fun getKeyByPath(hkey: WinReg.HKEY, path: String): Map<String, String>
}
class RegEditWindows : AbstractRegEdit() {
@@ -74,6 +75,19 @@ class RegEditWindows : AbstractRegEdit() {
}
return keysMap
}
override fun getKeyByPath(hkey: WinReg.HKEY, path: String): Map<String, String> {
val keysMap = mutableMapOf<String, String>()
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() {
@@ -141,6 +155,9 @@ class RegEditLinux : AbstractRegEdit() {
return keysMap
}
// This function should never run on Linux.
override fun getKeyByPath(hkey: WinReg.HKEY, path: String): Map<String, String> = mutableMapOf<String, String>()
companion object {
const val USER_REG_SUBPATH = "steamapps/compatdata/438100/pfx/user.reg"
val USER_REG_PATH =

View File

@@ -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'")
}
}
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}