mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-05 18:01:56 +02:00
Merge branch 'main' into aed/grammar-police
This commit is contained in:
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
@@ -34,6 +34,8 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin --recurse-submodules=no --force
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node
|
||||
@@ -71,6 +73,8 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin --recurse-submodules=no --force
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
@@ -98,6 +102,8 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin --recurse-submodules=no --force
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node
|
||||
@@ -129,6 +135,8 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin --recurse-submodules=no --force
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup Node
|
||||
@@ -201,6 +209,8 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin --recurse-submodules=no --force
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
@@ -231,6 +241,24 @@ jobs:
|
||||
name: release-android
|
||||
path: SlimeVR-android.apk
|
||||
|
||||
- name: Build Google Play release bundle
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: ./gradlew :server:android:bundleRelease
|
||||
env:
|
||||
ANDROID_STORE_FILE: ${{ secrets.ANDROID_GPLAY_STORE_FILE }}
|
||||
ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_GPLAY_STORE_PASSWD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_GPLAY_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_GPLAY_KEY_PASSWD }}
|
||||
|
||||
- name: Upload the Google Play artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
# Artifact name
|
||||
name: 'SlimeVR-Android-GPDev' # optional, default is artifact
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: server/android/build/outputs/bundle/release/*
|
||||
|
||||
create-release:
|
||||
name: Finalize Release Draft
|
||||
needs: [package-desktop, bundle-android, build-server-jar, build-gui-frontend]
|
||||
|
||||
@@ -14,7 +14,7 @@ import { IPC_CHANNELS } from '../shared';
|
||||
import path, { dirname, join } from 'path';
|
||||
import open from 'open';
|
||||
import trayIcon from '../resources/icons/icon.png?asset';
|
||||
import appleTrayIcon from '../resources/icons/appleTrayIcon.png?asset';
|
||||
import appleTrayIcon from '../resources/icons/Square30x30Logo.png?asset';
|
||||
import { readFile, stat } from 'fs/promises';
|
||||
import { getPlatform, handleIpc, isPortAvailable } from './utils';
|
||||
import {
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
getWindowStateFile,
|
||||
} from './paths';
|
||||
import { stores } from './store';
|
||||
import { logger } from './logger';
|
||||
import { closeLogger, logger } from './logger';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
@@ -36,11 +36,16 @@ import { ServerStatusEvent } from 'electron/preload/interface';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { MenuItem } from 'electron/main';
|
||||
|
||||
// Fixes colors looking washed on linux
|
||||
// Might affect hdr
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('disable-features', 'WaylandWpColorManagerV1');
|
||||
app.commandLine.appendSwitch('force-color-profile', 'srgb');
|
||||
}
|
||||
|
||||
app.setPath('userData', getGuiDataFolder())
|
||||
app.setPath('sessionData', join(getGuiDataFolder(), 'electron'))
|
||||
app.setPath('userData', getGuiDataFolder());
|
||||
app.setPath('sessionData', join(getGuiDataFolder(), 'electron'));
|
||||
|
||||
// Register custom protocol to handle asset paths with leading slashes
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: 'app',
|
||||
@@ -268,6 +273,9 @@ function createWindow() {
|
||||
case 'close':
|
||||
mainWindow?.close();
|
||||
break;
|
||||
case 'hide':
|
||||
mainWindow?.hide();
|
||||
break;
|
||||
case 'minimize':
|
||||
mainWindow?.minimize();
|
||||
break;
|
||||
@@ -339,8 +347,7 @@ function createWindow() {
|
||||
menu.append(new MenuItem({ label: 'Copy', role: 'copy' }));
|
||||
menu.append(new MenuItem({ label: 'Paste', role: 'paste' }));
|
||||
|
||||
if (mainWindow)
|
||||
menu.popup({ window: mainWindow });
|
||||
if (mainWindow) menu.popup({ window: mainWindow });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -353,7 +360,7 @@ const checkEnvironmentVariables = () => {
|
||||
'SlimeVR',
|
||||
`You have environment variables ${set.join(', ')} set, which may cause the SlimeVR Server to fail to launch properly.`
|
||||
);
|
||||
app.exit(0);
|
||||
app.quit();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -380,36 +387,60 @@ const spawnServer = async () => {
|
||||
'SlimeVR',
|
||||
`Couldn't find a compatible Java version, please download Java 17 or higher`
|
||||
);
|
||||
app.exit(0);
|
||||
app.quit()
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({ javaBin, serverJar }, 'Found Java and server jar');
|
||||
|
||||
const process = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run']);
|
||||
|
||||
process.stdout?.on('data', (message) => {
|
||||
mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, {
|
||||
message: message.toString(),
|
||||
type: 'stdout',
|
||||
} satisfies ServerStatusEvent);
|
||||
const platform = getPlatform();
|
||||
const serverWorkdir = getServerDataFolder()
|
||||
const serverProcess = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run'], {
|
||||
cwd: serverWorkdir,
|
||||
shell: false,
|
||||
env:
|
||||
platform === 'windows'
|
||||
? {
|
||||
...process.env,
|
||||
APPDATA: app.getPath('appData'),
|
||||
LOCALAPPDATA: process.env['USERPROFILE'] ? path.join(process.env['USERPROFILE'], 'AppData', 'Local') : undefined,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
process.stderr?.on('data', (message) => {
|
||||
mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, {
|
||||
message: message.toString(),
|
||||
type: 'stderr',
|
||||
} satisfies ServerStatusEvent);
|
||||
const sendToWindow = (event: ServerStatusEvent) => {
|
||||
if (mainWindow && !mainWindow.webContents.isDestroyed()) {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.SERVER_STATUS, event);
|
||||
}
|
||||
};
|
||||
|
||||
serverProcess.stdout?.on('data', (message) => {
|
||||
sendToWindow({ message: message.toString(), type: 'stdout' });
|
||||
});
|
||||
|
||||
serverProcess.stderr?.on('data', (message) => {
|
||||
sendToWindow({ message: message.toString(), type: 'stderr' });
|
||||
});
|
||||
|
||||
serverProcess.on('error', (err) => {
|
||||
logger.info({ err }, 'Error launching the java server');
|
||||
if (!isQuitting) app.quit();
|
||||
})
|
||||
|
||||
serverProcess.on('exit', () => {
|
||||
logger.info('Server process exiting');
|
||||
})
|
||||
|
||||
const exited = new Promise<void>((resolve) => serverProcess.once('exit', resolve));
|
||||
|
||||
return {
|
||||
process: process,
|
||||
close: () => {
|
||||
process.kill('SIGTERM');
|
||||
},
|
||||
process: serverProcess,
|
||||
close: () => serverProcess.kill(),
|
||||
waitForExit: () => exited,
|
||||
};
|
||||
};
|
||||
|
||||
let isQuitting = false;
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Register protocol handler for app:// scheme to handle assets with leading slashes
|
||||
protocol.handle('app', (request) => {
|
||||
@@ -426,23 +457,21 @@ app.whenReady().then(async () => {
|
||||
logger.info('SlimeVR started!');
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
app.quit();
|
||||
});
|
||||
|
||||
process.on('exit', () => {
|
||||
server?.close();
|
||||
});
|
||||
|
||||
app.on('before-quit', async () => {
|
||||
app.on('before-quit', async (event) => {
|
||||
if (isQuitting) return;
|
||||
isQuitting = true;
|
||||
event.preventDefault();
|
||||
logger.info('App quitting, saving...');
|
||||
server?.close();
|
||||
await server?.waitForExit();
|
||||
stores.settings.save();
|
||||
stores.cache.save();
|
||||
|
||||
discordPresence.destroy();
|
||||
|
||||
await saveWindowState();
|
||||
await closeLogger();
|
||||
app.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,3 +24,11 @@ const transport = pino.transport({
|
||||
});
|
||||
|
||||
export const logger = pino(transport);
|
||||
|
||||
export const closeLogger = () =>
|
||||
new Promise<void>((resolve) => {
|
||||
logger.flush(() => {
|
||||
transport.once('close', resolve);
|
||||
transport.end();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,12 +103,12 @@ export const findSystemJRE = async (sharedDir: string) => {
|
||||
export const findServerJar = () => {
|
||||
const paths = [
|
||||
options.path ? path.resolve(options.path) : undefined,
|
||||
app.isPackaged ? path.resolve(process.resourcesPath) : undefined,
|
||||
// AppImage passes the fakeroot in `APPDIR` env var.
|
||||
process.env['APPDIR']
|
||||
? path.resolve(join(process.env['APPDIR'], 'usr/share/slimevr/'))
|
||||
: undefined,
|
||||
path.dirname(app.getPath('exe')),
|
||||
|
||||
// For flatpack container
|
||||
path.resolve('/app/share/slimevr/'),
|
||||
path.resolve('/usr/share/slimevr/'),
|
||||
|
||||
@@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
openUrl: (url) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_URL, url),
|
||||
osStats: () => ipcRenderer.invoke(IPC_CHANNELS.OS_STATS),
|
||||
close: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'close'),
|
||||
hide: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'hide'),
|
||||
minimize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'minimize'),
|
||||
maximize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'maximize'),
|
||||
getStorage: async (type) => {
|
||||
|
||||
1
gui/electron/preload/interface.d.ts
vendored
1
gui/electron/preload/interface.d.ts
vendored
@@ -43,6 +43,7 @@ export interface IElectronAPI {
|
||||
openLogsFolder: () => Promise<void>;
|
||||
openConfigFolder: () => Promise<void>;
|
||||
close: () => void;
|
||||
hide: () => void;
|
||||
minimize: () => void;
|
||||
maximize: () => void;
|
||||
showDecorations: (decorations: boolean) => void;
|
||||
|
||||
@@ -25,7 +25,7 @@ export const IPC_CHANNELS = {
|
||||
export interface IpcInvokeMap {
|
||||
[IPC_CHANNELS.OPEN_URL]: (url: string) => void;
|
||||
[IPC_CHANNELS.OS_STATS]: () => Promise<OSStats>;
|
||||
[IPC_CHANNELS.WINDOW_ACTIONS]: (action: 'close' | 'minimize' | 'maximize') => void;
|
||||
[IPC_CHANNELS.WINDOW_ACTIONS]: (action: 'close' | 'minimize' | 'maximize' | 'hide') => void;
|
||||
[IPC_CHANNELS.LOG]: (type: 'info' | 'error' | 'warn', ...args: unknown[]) => void;
|
||||
[IPC_CHANNELS.OPEN_DIALOG]: (
|
||||
options: OpenDialogOptions
|
||||
|
||||
@@ -81,7 +81,7 @@ export function TopBar({
|
||||
}
|
||||
|
||||
if (config?.useTray && !dontTray) {
|
||||
electron.api.minimize();
|
||||
electron.api.hide();
|
||||
} else if (
|
||||
config?.connectedTrackersWarning &&
|
||||
connectedIMUTrackers.filter(
|
||||
|
||||
@@ -160,7 +160,7 @@ android {
|
||||
|
||||
// adds an offset of the version code as we might do apk releases in the middle of actual
|
||||
// releases if we failed on bundling or stuff
|
||||
val versionCodeOffset = 4
|
||||
val versionCodeOffset = 5
|
||||
// Defines the version number of your app.
|
||||
versionCode = (extra["gitVersionCode"] as? Int)?.plus(versionCodeOffset) ?: 0
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ dependencies {
|
||||
implementation("org.apache.commons:commons-lang3:3.20.0")
|
||||
implementation("org.apache.commons:commons-collections4:4.5.0")
|
||||
|
||||
implementation("com.illposed.osc:javaosc-core:0.9")
|
||||
implementation("com.illposed.osc:javaosc-core:0.8")
|
||||
implementation("org.java-websocket:Java-WebSocket:1.+")
|
||||
implementation("com.melloware:jintellitype:1.+")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
|
||||
|
||||
@@ -19,6 +19,10 @@ class BVHRecorder(server: VRServer) {
|
||||
val file = if (filePath.isDirectory()) {
|
||||
getBvhFile(filePath) ?: return
|
||||
} else {
|
||||
if (filePath.extension != "bvh") {
|
||||
LogManager.severe("[BVH] Invalid file extension for bvh file \"${filePath}\".")
|
||||
return
|
||||
}
|
||||
filePath
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,18 @@ class UDPDevice(
|
||||
@JvmField
|
||||
var lastPacketNumber: Long = -1
|
||||
|
||||
@JvmField
|
||||
var totalPacketsReceived: Int = 0
|
||||
|
||||
@JvmField
|
||||
var acceptedPackets: Int = 0
|
||||
|
||||
@JvmField
|
||||
var lastPacketCounterReset: Long = System.currentTimeMillis()
|
||||
|
||||
val packetLossPercent: Float
|
||||
get() = if (totalPacketsReceived == 0) 0f else (1f - acceptedPackets.toFloat() / totalPacketsReceived.toFloat())
|
||||
|
||||
@JvmField
|
||||
var protocol: NetworkProtocol? = null
|
||||
|
||||
@@ -68,9 +80,25 @@ class UDPDevice(
|
||||
var firmwareFeatures = FirmwareFeatures()
|
||||
|
||||
fun isNextPacket(packetId: Long): Boolean {
|
||||
if (packetId != 0L && packetId <= lastPacketNumber) return false
|
||||
lastPacketNumber = packetId
|
||||
return true
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastPacketCounterReset >= 10_000L) {
|
||||
totalPacketsReceived = 0
|
||||
acceptedPackets = 0
|
||||
lastPacketCounterReset = now
|
||||
}
|
||||
totalPacketsReceived++
|
||||
val accepted = packetId == 0L || packetId > lastPacketNumber
|
||||
if (accepted) {
|
||||
lastPacketNumber = packetId
|
||||
acceptedPackets++
|
||||
}
|
||||
val lost = totalPacketsReceived - acceptedPackets
|
||||
trackers.values.forEach {
|
||||
it.packetsReceived = totalPacketsReceived
|
||||
it.packetsLost = lost
|
||||
it.packetLoss = packetLossPercent
|
||||
}
|
||||
return accepted
|
||||
}
|
||||
|
||||
override fun toString(): String = "udp:/$ipAddress"
|
||||
|
||||
Reference in New Issue
Block a user