diff --git a/Dotnet/AppApi/Cef/AppApiCef.cs b/Dotnet/AppApi/Cef/AppApiCef.cs index 7e8cd79b..1c38f594 100644 --- a/Dotnet/AppApi/Cef/AppApiCef.cs +++ b/Dotnet/AppApi/Cef/AppApiCef.cs @@ -147,13 +147,6 @@ namespace VRCX Program.VRCXVRInstance.ExecuteVrOverlayFunction(function, json); } - public override string GetLaunchCommand() - { - var command = StartupArgs.LaunchArguments.LaunchCommand; - StartupArgs.LaunchArguments.LaunchCommand = string.Empty; - return command; - } - public override void FocusWindow() { MainForm.Instance.Invoke(new Action(() => { MainForm.Instance.Focus_Window(); })); diff --git a/Dotnet/AppApi/Common/AppApiCommon.cs b/Dotnet/AppApi/Common/AppApiCommon.cs index bf342b6f..2b302b6a 100644 --- a/Dotnet/AppApi/Common/AppApiCommon.cs +++ b/Dotnet/AppApi/Common/AppApiCommon.cs @@ -60,6 +60,13 @@ namespace VRCX }); } } + + public string GetLaunchCommand() + { + var command = StartupArgs.LaunchArguments.LaunchCommand; + StartupArgs.LaunchArguments.LaunchCommand = string.Empty; + return command; + } public void IPCAnnounceStart() { diff --git a/Dotnet/AppApi/Common/AppApiCommonBase.cs b/Dotnet/AppApi/Common/AppApiCommonBase.cs index 7fe93d3f..7553a27a 100644 --- a/Dotnet/AppApi/Common/AppApiCommonBase.cs +++ b/Dotnet/AppApi/Common/AppApiCommonBase.cs @@ -21,7 +21,6 @@ namespace VRCX public abstract void ExecuteAppFunction(string function, string json); public abstract void ExecuteVrFeedFunction(string function, string json); public abstract void ExecuteVrOverlayFunction(string function, string json); - public abstract string GetLaunchCommand(); public abstract void FocusWindow(); public abstract void ChangeTheme(int value); public abstract void DoFunny(); diff --git a/Dotnet/AppApi/Electron/AppApiElectron.cs b/Dotnet/AppApi/Electron/AppApiElectron.cs index ff9422db..b6123cf6 100644 --- a/Dotnet/AppApi/Electron/AppApiElectron.cs +++ b/Dotnet/AppApi/Electron/AppApiElectron.cs @@ -64,11 +64,6 @@ namespace VRCX { } - public override string GetLaunchCommand() - { - return string.Empty; - } - public override void FocusWindow() { } diff --git a/Dotnet/AppApi/Electron/RegistryPlayerPrefs.cs b/Dotnet/AppApi/Electron/RegistryPlayerPrefs.cs index ec13111a..091921e0 100644 --- a/Dotnet/AppApi/Electron/RegistryPlayerPrefs.cs +++ b/Dotnet/AppApi/Electron/RegistryPlayerPrefs.cs @@ -45,7 +45,7 @@ namespace VRCX { var compatToolMapping = new Dictionary(); const string sectionHeader = "\"CompatToolMapping\""; - int sectionStart = vdfContent.IndexOf(sectionHeader); + int sectionStart = vdfContent.IndexOf(sectionHeader, StringComparison.Ordinal); if (sectionStart == -1) { @@ -53,7 +53,7 @@ namespace VRCX return compatToolMapping; } - int blockStart = vdfContent.IndexOf("{", sectionStart) + 1; + int blockStart = vdfContent.IndexOf('{', sectionStart) + 1; int blockEnd = FindMatchingBracket(vdfContent, blockStart - 1); if (blockStart == -1 || blockEnd == -1) @@ -317,6 +317,12 @@ namespace VRCX { string winePath = GetVRChatWinePath(); string winePrefix = _vrcPrefixPath; + if (string.IsNullOrEmpty(winePath) || string.IsNullOrEmpty(winePrefix)) + { + logger.Info("VRC Wine path was not found"); + return null; + } + string wineRegCommand = $"\"{winePath}\" reg {command}"; ProcessStartInfo processStartInfo = GetWineProcessStartInfo(winePath, winePrefix, wineRegCommand); using var process = Process.Start(processStartInfo); diff --git a/Dotnet/Program.cs b/Dotnet/Program.cs index 0ce95daf..340783cc 100644 --- a/Dotnet/Program.cs +++ b/Dotnet/Program.cs @@ -9,6 +9,7 @@ using NLog.Targets; using System; using System.Data.SQLite; using System.IO; +using System.Text.Json; using System.Threading; using System.Windows.Forms; @@ -224,6 +225,9 @@ namespace VRCX Application.SetCompatibleTextRenderingDefault(false); logger.Info("{0} Starting...", Version); + logger.Info("Args: {0}", JsonSerializer.Serialize(StartupArgs.Args)); + if (!string.IsNullOrEmpty(StartupArgs.LaunchArguments.LaunchCommand)) + logger.Info("Launch Command: {0}", StartupArgs.LaunchArguments.LaunchCommand); logger.Debug("Wine detection: {0}", Wine.GetIfWine()); SQLiteLegacy.Instance.Init(); @@ -276,6 +280,9 @@ namespace VRCX Update.Check(); logger.Info("{0} Starting...", Version); + logger.Info("Args: {0}", JsonSerializer.Serialize(StartupArgs.Args)); + if (!string.IsNullOrEmpty(StartupArgs.LaunchArguments.LaunchCommand)) + logger.Info("Launch Command: {0}", StartupArgs.LaunchArguments.LaunchCommand); AppApiInstance = new AppApiElectron(); // ProcessMonitor.Instance.Init(); diff --git a/Dotnet/StartupArgs.cs b/Dotnet/StartupArgs.cs index 75799e2f..47f556eb 100644 --- a/Dotnet/StartupArgs.cs +++ b/Dotnet/StartupArgs.cs @@ -11,6 +11,7 @@ using System.IO.Pipes; using System.Linq; using System.Management; using System.Text; +using System.Text.Json; using System.Threading; #if !LINUX @@ -24,9 +25,11 @@ namespace VRCX { private const string SubProcessTypeArgument = "--type"; public static VrcxLaunchArguments LaunchArguments = new(); + public static string[] Args; public static void ArgsCheck(string[] args) { + Args = args; Debug.Assert(Program.LaunchDebug = true); LaunchArguments = ParseArgs(args); @@ -77,6 +80,9 @@ namespace VRCX if (arg.StartsWith(VrcxLaunchArguments.LaunchCommandPrefix) && arg.Length > VrcxLaunchArguments.LaunchCommandPrefix.Length) arguments.LaunchCommand = arg.Substring(VrcxLaunchArguments.LaunchCommandPrefix.Length); + + if (arg.StartsWith(VrcxLaunchArguments.LinuxLaunchCommandPrefix) && arg.Length > VrcxLaunchArguments.LinuxLaunchCommandPrefix.Length) + arguments.LaunchCommand = arg.Substring(VrcxLaunchArguments.LinuxLaunchCommandPrefix.Length); if (arg.StartsWith(VrcxLaunchArguments.ConfigDirectoryPrefix) && arg.Length > VrcxLaunchArguments.ConfigDirectoryPrefix.Length) arguments.ConfigDirectory = arg.Substring(VrcxLaunchArguments.ConfigDirectoryPrefix.Length + 1); @@ -96,6 +102,7 @@ namespace VRCX public bool IsDebug { get; set; } = false; public const string LaunchCommandPrefix = "/uri=vrcx://"; + public const string LinuxLaunchCommandPrefix = "vrcx://"; public string LaunchCommand { get; set; } = null; public const string ConfigDirectoryPrefix = "--config"; diff --git a/Dotnet/WebApi.cs b/Dotnet/WebApi.cs index 0ddf28b1..c31e4cec 100644 --- a/Dotnet/WebApi.cs +++ b/Dotnet/WebApi.cs @@ -359,7 +359,6 @@ namespace VRCX public async Task ExecuteJson(string options) { var data = JsonConvert.DeserializeObject>(options); - Logger.Info(JsonConvert.SerializeObject(data)); var result = await Execute(data); return System.Text.Json.JsonSerializer.Serialize(new { diff --git a/src-electron/main.js b/src-electron/main.js index 94e16fac..dcd5daff 100644 --- a/src-electron/main.js +++ b/src-electron/main.js @@ -9,7 +9,7 @@ const { dialog, Notification } = require('electron'); -const { spawn } = require('child_process'); +const { spawn, spawnSync } = require('child_process'); const fs = require('fs'); const https = require('https'); @@ -23,10 +23,12 @@ if (!isDotNetInstalled()) { } // get launch arguments +let appImagePath = process.env.APPIMAGE; const args = process.argv.slice(1); -const noInstall = args.some((val) => val === '--no-install'); -const x11 = args.some((val) => val === '--x11'); +const noInstall = args.includes('--no-install'); +const x11 = args.includes('--x11'); const homePath = getHomePath(); +tryRelaunchWithArgs(args); tryCopyFromWinePrefix(); const rootDir = app.getAppPath(); @@ -52,8 +54,9 @@ ipcMain.handle('callDotNetMethod', (event, className, methodName, args) => { let mainWindow = undefined; const VRCXStorage = interopApi.getDotNetObject('VRCXStorage'); +const hasAskedToMoveAppImage = + VRCXStorage.Get('VRCX_HasAskedToMoveAppImage') === 'true'; let isCloseToTray = VRCXStorage.Get('VRCX_CloseToTray') === 'true'; -let appImagePath = process.env.APPIMAGE; ipcMain.handle('applyWindowSettings', (event, position, size, state) => { if (position) { @@ -107,10 +110,15 @@ ipcMain.handle('notification:showNotification', (event, title, body, icon) => { ipcMain.handle('app:restart', () => { if (process.platform === 'linux') { - const options = { args: process.argv.slice(1) }; + const options = { + execPath: process.execPath, + args: process.argv.slice(1) + }; if (appImagePath) { options.execPath = appImagePath; - options.args.unshift('--appimage-extract-and-run'); + if (!x11 && !options.args.includes('--appimage-extract-and-run')) { + options.args.unshift('--appimage-extract-and-run'); + } } app.relaunch(options); app.exit(0); @@ -120,9 +128,12 @@ ipcMain.handle('app:restart', () => { } }); -function relaunchWithArgs(args) { - if (process.argv.includes('--ozone-platform-hint=auto')) { - console.log('Already running with correct arguments'); +function tryRelaunchWithArgs(args) { + if ( + process.platform !== 'linux' || + x11 || + args.includes('--ozone-platform-hint=auto') + ) { return; } @@ -148,14 +159,6 @@ function relaunchWithArgs(args) { } function createWindow() { - if ( - process.platform === 'linux' && - !process.argv.includes('--ozone-platform-hint=auto') && - !x11 - ) { - relaunchWithArgs(process.argv.slice(1)); - } - app.commandLine.appendSwitch('enable-speech-dispatcher'); const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0; @@ -180,7 +183,7 @@ function createWindow() { const indexPath = path.join(rootDir, 'build/html/index.html'); mainWindow.loadFile(indexPath, { userAgent: version }); - // add proxy config + // add proxy config, doesn't work, thanks electron // const proxy = VRCXStorage.Get('VRCX_Proxy'); // if (proxy) { // session.setProxy( @@ -294,35 +297,6 @@ function createTray() { }); } -/* -async function installVRCXappImageLauncher() { - const iconUrl = - 'https://raw.githubusercontent.com/vrcx-team/VRCX/master/VRCX.png'; - - let targetIconName; - const desktopFiles = fs.readdirSync( - path.join(homePath, '.local/share/applications') - ); - for (const file of desktopFiles) { - if (file.includes('appimagekit_') && file.includes('VRCX')) { - console.log('AppImageLauncher shortcut found:', file); - targetIconName = file.replace('.desktop', '.png'); - targetIconName = targetIconName.replace('-', '_'); - try { - } catch (err) { - console.error('Error deleting shortcut:', err); - return; - } - } - } - - const iconPath = '~/.local/share/icons/' + targetIconName; - const expandedPath = iconPath.replace('~', process.env.HOME); - const targetIconPath = path.join(expandedPath); - await downloadIcon(iconUrl, targetIconPath); -} -*/ - async function installVRCX() { console.log('Home path:', homePath); console.log('AppImage path:', appImagePath); @@ -331,121 +305,144 @@ async function installVRCX() { return; } if (noInstall) { + interopApi.getDotNetObject('Update').Init(appImagePath); console.log('Skipping installation.'); return; } - /* - let appImageLauncherInstalled = false; - if (fs.existsSync('/usr/bin/AppImageLauncher')) { - appImageLauncherInstalled = true; - } - */ - - if (appImagePath.startsWith(path.join(homePath, 'Applications'))) { - /* - if (appImageLauncherInstalled) { - installVRCXappImageLauncher(); - } - */ - interopApi.getDotNetObject('Update').Init(appImagePath); - console.log('VRCX is already installed.'); - return; - } - - let currentName = path.basename(appImagePath); - let newName = 'VRCX.AppImage'; - if (currentName !== newName) { - const newPath = path.join(path.dirname(appImagePath), newName); + // rename AppImage to VRCX.AppImage + const currentName = path.basename(appImagePath); + const expectedName = 'VRCX.AppImage'; + if (currentName !== expectedName) { + const newPath = path.join(path.dirname(appImagePath), expectedName); try { + // remove existing VRCX.AppImage + if (fs.existsSync(newPath)) { + fs.unlinkSync(newPath); + } fs.renameSync(appImagePath, newPath); console.log('AppImage renamed to:', newPath); appImagePath = newPath; } catch (err) { - console.error('Error renaming AppImage:', err); - dialog.showErrorBox('VRCX', 'Failed to rename AppImage.'); + console.error(`Error renaming AppImage ${newPath}`, err); + dialog.showErrorBox('VRCX', `Failed to rename AppImage ${newPath}`); return; } } - if ( - process.env.APPIMAGE.startsWith(path.join(homePath, 'Applications')) && - path.basename(process.env.APPIMAGE) === 'VRCX.AppImage' - ) { - interopApi.getDotNetObject('Update').Init(appImagePath); - console.log('VRCX is already installed.'); - return; - } - - const targetPath = path.join(homePath, 'Applications'); - console.log('Target Path:', targetPath); - - // Create target directory if it doesn't exist - if (!fs.existsSync(targetPath)) { - fs.mkdirSync(targetPath); - } - - const targetAppImagePath = path.join(targetPath, 'VRCX.AppImage'); - - // Move the AppImage to the target directory - try { - if (fs.existsSync(targetAppImagePath)) { - fs.unlinkSync(targetAppImagePath); + // ask to move AppImage to ~/Applications + const appImageHomePath = `${homePath}/Applications/VRCX.AppImage`; + if (!hasAskedToMoveAppImage && appImagePath !== appImageHomePath) { + const result = dialog.showMessageBoxSync(mainWindow, { + type: 'question', + title: 'VRCX', + message: 'Do you want to install VRCX?', + detail: 'VRCX will be moved to your ~/Applications folder.', + buttons: ['No', 'Yes'] + }); + if (result === 0) { + console.log('Cancel AppImage move to ~/Applications'); + // don't ask again + VRCXStorage.Set('VRCX_HasAskedToMoveAppImage', 'true'); + VRCXStorage.Save(); + } + if (result === 1) { + console.log('Moving AppImage to ~/Applications'); + try { + const applicationsPath = path.join(homePath, 'Applications'); + // create ~/Applications if it doesn't exist + if (!fs.existsSync(applicationsPath)) { + fs.mkdirSync(applicationsPath); + } + // remove existing VRCX.AppImage + if (fs.existsSync(appImageHomePath)) { + fs.unlinkSync(appImageHomePath); + } + fs.renameSync(appImagePath, appImageHomePath); + appImagePath = appImageHomePath; + console.log('AppImage moved to:', appImageHomePath); + } catch (err) { + console.error(`Error moving AppImage ${appImageHomePath}`, err); + dialog.showErrorBox( + 'VRCX', + `Failed to move AppImage ${appImageHomePath}` + ); + return; + } } - fs.renameSync(appImagePath, targetAppImagePath); - appImagePath = targetAppImagePath; - console.log('AppImage moved to:', targetAppImagePath); - } catch (err) { - console.error('Error moving AppImage:', err); - dialog.showErrorBox('VRCX', 'Failed to move AppImage.'); - return; } + // inform .NET side about AppImage path + interopApi.getDotNetObject('Update').Init(appImagePath); + await createDesktopFile(); - dialog.showMessageBox({ - type: 'info', - title: 'VRCX', - message: 'VRCX has been installed successfully.', - detail: 'You can now find VRCX in your ~/Applications folder.' - }); } async function createDesktopFile() { // Download the icon and save it to the target directory - const iconUrl = - 'https://raw.githubusercontent.com/vrcx-team/VRCX/master/VRCX.png'; const iconPath = path.join(homePath, '.local/share/icons/VRCX.png'); - await downloadIcon(iconUrl, iconPath) - .then(() => { - console.log('Icon downloaded and saved to:', iconPath); - const desktopFile = `[Desktop Entry] -Name=VRCX -Comment=Friendship management tool for VRChat -Exec=${appImagePath} --ozone-platform-hint=auto -Icon=VRCX -Type=Application -Categories=Network;InstantMessaging;Game; -Terminal=false -StartupWMClass=VRCX -`; + if (!fs.existsSync(iconPath)) { + const iconUrl = + 'https://raw.githubusercontent.com/vrcx-team/VRCX/master/VRCX.png'; + await downloadIcon(iconUrl, iconPath) + .then(() => { + console.log('Icon downloaded and saved to:', iconPath); + }) + .catch((err) => { + console.error('Error downloading icon:', err); + dialog.showErrorBox('VRCX', 'Failed to download the icon.'); + }); + } - const desktopFilePath = path.join( - homePath, - '.local/share/applications/VRCX.desktop' + // Create the desktop file + const desktopFilePath = path.join( + homePath, + '.local/share/applications/VRCX.desktop' + ); + const dotDesktop = { + Name: 'VRCX', + Comment: 'Friendship management tool for VRChat', + Exec: `${appImagePath} --ozone-platform-hint=auto %U`, + Icon: 'VRCX', + Type: 'Application', + Categories: 'Network;InstantMessaging;Game;', + Terminal: 'false', + StartupWMClass: 'VRCX', + MimeType: 'x-scheme-handler/vrcx;' + }; + const desktopFile = + '[Desktop Entry]\n' + + Object.entries(dotDesktop) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + try { + // create/update the desktop file when needed + let existingDesktopFile = ''; + if (fs.existsSync(desktopFilePath)) { + existingDesktopFile = fs.readFileSync(desktopFilePath, 'utf8'); + } + if (existingDesktopFile !== desktopFile) { + fs.writeFileSync(desktopFilePath, desktopFile); + console.log('Desktop file created at:', desktopFilePath); + + const result = spawnSync( + 'xdg-mime', + ['default', 'VRCX.desktop', 'x-scheme-handler/vrcx'], + { + encoding: 'utf-8' + } ); - try { - fs.writeFileSync(desktopFilePath, desktopFile); - console.log('Desktop file created at:', desktopFilePath); - } catch (err) { - console.error('Error creating desktop file:', err); - dialog.showErrorBox('VRCX', 'Failed to create desktop entry.'); - return; + if (result.error) { + console.error('Error setting MIME type:', result.error); + } else { + console.log('MIME type set x-scheme-handler/vrcx'); } - }) - .catch((err) => { - console.error('Error downloading icon:', err); - dialog.showErrorBox('VRCX', 'Failed to download the icon.'); - }); + } + } catch (err) { + console.error('Error creating desktop file:', err); + dialog.showErrorBox('VRCX', 'Failed to create desktop entry.'); + return; + } } function downloadIcon(url, targetPath) { @@ -501,7 +498,7 @@ function getVersion() { // look for trailing git hash "-22bcd96" to indicate nightly build var version = versionFile.split('-'); - console.log('Version:', version); + console.log('Version:', versionFile); if (version.length > 0 && version[version.length - 1].length == 7) { return `VRCX (Linux) Nightly ${versionFile}`; } else { @@ -518,13 +515,9 @@ function isDotNetInstalled() { // Assume .NET is already installed on macOS return true; } - const result = require('child_process').spawnSync( - 'dotnet', - ['--list-runtimes'], - { - encoding: 'utf-8' - } - ); + const result = spawnSync('dotnet', ['--list-runtimes'], { + encoding: 'utf-8' + }); return result.stdout?.includes('.NETCore.App 9.0'); }