mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Merge remote-tracking branch 'origin/hannah/steam' into hannah/keybinds
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
1
gui/electron/preload/interface.d.ts
vendored
1
gui/electron/preload/interface.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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']]>;
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
113
gui/src/components/onboarding/UdevRulesModal.tsx
Normal file
113
gui/src/components/onboarding/UdevRulesModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
6
server/core/src/main/java/dev/slimevr/FeatureFlags.kt
Normal file
6
server/core/src/main/java/dev/slimevr/FeatureFlags.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
package dev.slimevr
|
||||
|
||||
data class FeatureFlags(
|
||||
var steam: Boolean = false,
|
||||
var skipCheckUdev: Boolean = false,
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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'")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Submodule solarxr-protocol updated: 1905423523...bb6f13be59
Reference in New Issue
Block a user