Linux: Change how AppImage moving is handled, add support for vrcx:\\

This commit is contained in:
Natsumi
2025-02-28 09:53:43 +13:00
parent 641dba2fe9
commit 137608b705
9 changed files with 167 additions and 161 deletions
-7
View File
@@ -147,13 +147,6 @@ namespace VRCX
Program.VRCXVRInstance.ExecuteVrOverlayFunction(function, json); 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() public override void FocusWindow()
{ {
MainForm.Instance.Invoke(new Action(() => { MainForm.Instance.Focus_Window(); })); MainForm.Instance.Invoke(new Action(() => { MainForm.Instance.Focus_Window(); }));
+7
View File
@@ -61,6 +61,13 @@ namespace VRCX
} }
} }
public string GetLaunchCommand()
{
var command = StartupArgs.LaunchArguments.LaunchCommand;
StartupArgs.LaunchArguments.LaunchCommand = string.Empty;
return command;
}
public void IPCAnnounceStart() public void IPCAnnounceStart()
{ {
IPCServer.Send(new IPCPacket IPCServer.Send(new IPCPacket
-1
View File
@@ -21,7 +21,6 @@ namespace VRCX
public abstract void ExecuteAppFunction(string function, string json); public abstract void ExecuteAppFunction(string function, string json);
public abstract void ExecuteVrFeedFunction(string function, string json); public abstract void ExecuteVrFeedFunction(string function, string json);
public abstract void ExecuteVrOverlayFunction(string function, string json); public abstract void ExecuteVrOverlayFunction(string function, string json);
public abstract string GetLaunchCommand();
public abstract void FocusWindow(); public abstract void FocusWindow();
public abstract void ChangeTheme(int value); public abstract void ChangeTheme(int value);
public abstract void DoFunny(); public abstract void DoFunny();
-5
View File
@@ -64,11 +64,6 @@ namespace VRCX
{ {
} }
public override string GetLaunchCommand()
{
return string.Empty;
}
public override void FocusWindow() public override void FocusWindow()
{ {
} }
@@ -45,7 +45,7 @@ namespace VRCX
{ {
var compatToolMapping = new Dictionary<string, string>(); var compatToolMapping = new Dictionary<string, string>();
const string sectionHeader = "\"CompatToolMapping\""; const string sectionHeader = "\"CompatToolMapping\"";
int sectionStart = vdfContent.IndexOf(sectionHeader); int sectionStart = vdfContent.IndexOf(sectionHeader, StringComparison.Ordinal);
if (sectionStart == -1) if (sectionStart == -1)
{ {
@@ -53,7 +53,7 @@ namespace VRCX
return compatToolMapping; return compatToolMapping;
} }
int blockStart = vdfContent.IndexOf("{", sectionStart) + 1; int blockStart = vdfContent.IndexOf('{', sectionStart) + 1;
int blockEnd = FindMatchingBracket(vdfContent, blockStart - 1); int blockEnd = FindMatchingBracket(vdfContent, blockStart - 1);
if (blockStart == -1 || blockEnd == -1) if (blockStart == -1 || blockEnd == -1)
@@ -317,6 +317,12 @@ namespace VRCX
{ {
string winePath = GetVRChatWinePath(); string winePath = GetVRChatWinePath();
string winePrefix = _vrcPrefixPath; 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}"; string wineRegCommand = $"\"{winePath}\" reg {command}";
ProcessStartInfo processStartInfo = GetWineProcessStartInfo(winePath, winePrefix, wineRegCommand); ProcessStartInfo processStartInfo = GetWineProcessStartInfo(winePath, winePrefix, wineRegCommand);
using var process = Process.Start(processStartInfo); using var process = Process.Start(processStartInfo);
+7
View File
@@ -9,6 +9,7 @@ using NLog.Targets;
using System; using System;
using System.Data.SQLite; using System.Data.SQLite;
using System.IO; using System.IO;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
@@ -224,6 +225,9 @@ namespace VRCX
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);
logger.Info("{0} Starting...", Version); 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()); logger.Debug("Wine detection: {0}", Wine.GetIfWine());
SQLiteLegacy.Instance.Init(); SQLiteLegacy.Instance.Init();
@@ -276,6 +280,9 @@ namespace VRCX
Update.Check(); Update.Check();
logger.Info("{0} Starting...", Version); 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(); AppApiInstance = new AppApiElectron();
// ProcessMonitor.Instance.Init(); // ProcessMonitor.Instance.Init();
+7
View File
@@ -11,6 +11,7 @@ using System.IO.Pipes;
using System.Linq; using System.Linq;
using System.Management; using System.Management;
using System.Text; using System.Text;
using System.Text.Json;
using System.Threading; using System.Threading;
#if !LINUX #if !LINUX
@@ -24,9 +25,11 @@ namespace VRCX
{ {
private const string SubProcessTypeArgument = "--type"; private const string SubProcessTypeArgument = "--type";
public static VrcxLaunchArguments LaunchArguments = new(); public static VrcxLaunchArguments LaunchArguments = new();
public static string[] Args;
public static void ArgsCheck(string[] args) public static void ArgsCheck(string[] args)
{ {
Args = args;
Debug.Assert(Program.LaunchDebug = true); Debug.Assert(Program.LaunchDebug = true);
LaunchArguments = ParseArgs(args); LaunchArguments = ParseArgs(args);
@@ -78,6 +81,9 @@ namespace VRCX
if (arg.StartsWith(VrcxLaunchArguments.LaunchCommandPrefix) && arg.Length > VrcxLaunchArguments.LaunchCommandPrefix.Length) if (arg.StartsWith(VrcxLaunchArguments.LaunchCommandPrefix) && arg.Length > VrcxLaunchArguments.LaunchCommandPrefix.Length)
arguments.LaunchCommand = arg.Substring(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) if (arg.StartsWith(VrcxLaunchArguments.ConfigDirectoryPrefix) && arg.Length > VrcxLaunchArguments.ConfigDirectoryPrefix.Length)
arguments.ConfigDirectory = arg.Substring(VrcxLaunchArguments.ConfigDirectoryPrefix.Length + 1); arguments.ConfigDirectory = arg.Substring(VrcxLaunchArguments.ConfigDirectoryPrefix.Length + 1);
@@ -96,6 +102,7 @@ namespace VRCX
public bool IsDebug { get; set; } = false; public bool IsDebug { get; set; } = false;
public const string LaunchCommandPrefix = "/uri=vrcx://"; public const string LaunchCommandPrefix = "/uri=vrcx://";
public const string LinuxLaunchCommandPrefix = "vrcx://";
public string LaunchCommand { get; set; } = null; public string LaunchCommand { get; set; } = null;
public const string ConfigDirectoryPrefix = "--config"; public const string ConfigDirectoryPrefix = "--config";
-1
View File
@@ -359,7 +359,6 @@ namespace VRCX
public async Task<string> ExecuteJson(string options) public async Task<string> ExecuteJson(string options)
{ {
var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(options); var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(options);
Logger.Info(JsonConvert.SerializeObject(data));
var result = await Execute(data); var result = await Execute(data);
return System.Text.Json.JsonSerializer.Serialize(new return System.Text.Json.JsonSerializer.Serialize(new
{ {
+138 -145
View File
@@ -9,7 +9,7 @@ const {
dialog, dialog,
Notification Notification
} = require('electron'); } = require('electron');
const { spawn } = require('child_process'); const { spawn, spawnSync } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const https = require('https'); const https = require('https');
@@ -23,10 +23,12 @@ if (!isDotNetInstalled()) {
} }
// get launch arguments // get launch arguments
let appImagePath = process.env.APPIMAGE;
const args = process.argv.slice(1); const args = process.argv.slice(1);
const noInstall = args.some((val) => val === '--no-install'); const noInstall = args.includes('--no-install');
const x11 = args.some((val) => val === '--x11'); const x11 = args.includes('--x11');
const homePath = getHomePath(); const homePath = getHomePath();
tryRelaunchWithArgs(args);
tryCopyFromWinePrefix(); tryCopyFromWinePrefix();
const rootDir = app.getAppPath(); const rootDir = app.getAppPath();
@@ -52,8 +54,9 @@ ipcMain.handle('callDotNetMethod', (event, className, methodName, args) => {
let mainWindow = undefined; let mainWindow = undefined;
const VRCXStorage = interopApi.getDotNetObject('VRCXStorage'); const VRCXStorage = interopApi.getDotNetObject('VRCXStorage');
const hasAskedToMoveAppImage =
VRCXStorage.Get('VRCX_HasAskedToMoveAppImage') === 'true';
let isCloseToTray = VRCXStorage.Get('VRCX_CloseToTray') === 'true'; let isCloseToTray = VRCXStorage.Get('VRCX_CloseToTray') === 'true';
let appImagePath = process.env.APPIMAGE;
ipcMain.handle('applyWindowSettings', (event, position, size, state) => { ipcMain.handle('applyWindowSettings', (event, position, size, state) => {
if (position) { if (position) {
@@ -107,10 +110,15 @@ ipcMain.handle('notification:showNotification', (event, title, body, icon) => {
ipcMain.handle('app:restart', () => { ipcMain.handle('app:restart', () => {
if (process.platform === 'linux') { if (process.platform === 'linux') {
const options = { args: process.argv.slice(1) }; const options = {
execPath: process.execPath,
args: process.argv.slice(1)
};
if (appImagePath) { if (appImagePath) {
options.execPath = 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.relaunch(options);
app.exit(0); app.exit(0);
@@ -120,9 +128,12 @@ ipcMain.handle('app:restart', () => {
} }
}); });
function relaunchWithArgs(args) { function tryRelaunchWithArgs(args) {
if (process.argv.includes('--ozone-platform-hint=auto')) { if (
console.log('Already running with correct arguments'); process.platform !== 'linux' ||
x11 ||
args.includes('--ozone-platform-hint=auto')
) {
return; return;
} }
@@ -148,14 +159,6 @@ function relaunchWithArgs(args) {
} }
function createWindow() { 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'); app.commandLine.appendSwitch('enable-speech-dispatcher');
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0; const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
@@ -180,7 +183,7 @@ function createWindow() {
const indexPath = path.join(rootDir, 'build/html/index.html'); const indexPath = path.join(rootDir, 'build/html/index.html');
mainWindow.loadFile(indexPath, { userAgent: version }); mainWindow.loadFile(indexPath, { userAgent: version });
// add proxy config // add proxy config, doesn't work, thanks electron
// const proxy = VRCXStorage.Get('VRCX_Proxy'); // const proxy = VRCXStorage.Get('VRCX_Proxy');
// if (proxy) { // if (proxy) {
// session.setProxy( // 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() { async function installVRCX() {
console.log('Home path:', homePath); console.log('Home path:', homePath);
console.log('AppImage path:', appImagePath); console.log('AppImage path:', appImagePath);
@@ -331,121 +305,144 @@ async function installVRCX() {
return; return;
} }
if (noInstall) { if (noInstall) {
interopApi.getDotNetObject('Update').Init(appImagePath);
console.log('Skipping installation.'); console.log('Skipping installation.');
return; return;
} }
/* // rename AppImage to VRCX.AppImage
let appImageLauncherInstalled = false; const currentName = path.basename(appImagePath);
if (fs.existsSync('/usr/bin/AppImageLauncher')) { const expectedName = 'VRCX.AppImage';
appImageLauncherInstalled = true; if (currentName !== expectedName) {
} const newPath = path.join(path.dirname(appImagePath), expectedName);
*/
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);
try { try {
// remove existing VRCX.AppImage
if (fs.existsSync(newPath)) {
fs.unlinkSync(newPath);
}
fs.renameSync(appImagePath, newPath); fs.renameSync(appImagePath, newPath);
console.log('AppImage renamed to:', newPath); console.log('AppImage renamed to:', newPath);
appImagePath = newPath; appImagePath = newPath;
} catch (err) { } catch (err) {
console.error('Error renaming AppImage:', err); console.error(`Error renaming AppImage ${newPath}`, err);
dialog.showErrorBox('VRCX', 'Failed to rename AppImage.'); dialog.showErrorBox('VRCX', `Failed to rename AppImage ${newPath}`);
return; return;
} }
} }
if ( // ask to move AppImage to ~/Applications
process.env.APPIMAGE.startsWith(path.join(homePath, 'Applications')) && const appImageHomePath = `${homePath}/Applications/VRCX.AppImage`;
path.basename(process.env.APPIMAGE) === 'VRCX.AppImage' if (!hasAskedToMoveAppImage && appImagePath !== appImageHomePath) {
) { const result = dialog.showMessageBoxSync(mainWindow, {
interopApi.getDotNetObject('Update').Init(appImagePath); type: 'question',
console.log('VRCX is already installed.'); title: 'VRCX',
return; message: 'Do you want to install VRCX?',
} detail: 'VRCX will be moved to your ~/Applications folder.',
buttons: ['No', 'Yes']
const targetPath = path.join(homePath, 'Applications'); });
console.log('Target Path:', targetPath); if (result === 0) {
console.log('Cancel AppImage move to ~/Applications');
// Create target directory if it doesn't exist // don't ask again
if (!fs.existsSync(targetPath)) { VRCXStorage.Set('VRCX_HasAskedToMoveAppImage', 'true');
fs.mkdirSync(targetPath); VRCXStorage.Save();
} }
if (result === 1) {
const targetAppImagePath = path.join(targetPath, 'VRCX.AppImage'); console.log('Moving AppImage to ~/Applications');
try {
// Move the AppImage to the target directory const applicationsPath = path.join(homePath, 'Applications');
try { // create ~/Applications if it doesn't exist
if (fs.existsSync(targetAppImagePath)) { if (!fs.existsSync(applicationsPath)) {
fs.unlinkSync(targetAppImagePath); 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(); 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() { async function createDesktopFile() {
// Download the icon and save it to the target directory // 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'); const iconPath = path.join(homePath, '.local/share/icons/VRCX.png');
await downloadIcon(iconUrl, iconPath) if (!fs.existsSync(iconPath)) {
.then(() => { const iconUrl =
console.log('Icon downloaded and saved to:', iconPath); 'https://raw.githubusercontent.com/vrcx-team/VRCX/master/VRCX.png';
const desktopFile = `[Desktop Entry] await downloadIcon(iconUrl, iconPath)
Name=VRCX .then(() => {
Comment=Friendship management tool for VRChat console.log('Icon downloaded and saved to:', iconPath);
Exec=${appImagePath} --ozone-platform-hint=auto })
Icon=VRCX .catch((err) => {
Type=Application console.error('Error downloading icon:', err);
Categories=Network;InstantMessaging;Game; dialog.showErrorBox('VRCX', 'Failed to download the icon.');
Terminal=false });
StartupWMClass=VRCX }
`;
const desktopFilePath = path.join( // Create the desktop file
homePath, const desktopFilePath = path.join(
'.local/share/applications/VRCX.desktop' 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 { if (result.error) {
fs.writeFileSync(desktopFilePath, desktopFile); console.error('Error setting MIME type:', result.error);
console.log('Desktop file created at:', desktopFilePath); } else {
} catch (err) { console.log('MIME type set x-scheme-handler/vrcx');
console.error('Error creating desktop file:', err);
dialog.showErrorBox('VRCX', 'Failed to create desktop entry.');
return;
} }
}) }
.catch((err) => { } catch (err) {
console.error('Error downloading icon:', err); console.error('Error creating desktop file:', err);
dialog.showErrorBox('VRCX', 'Failed to download the icon.'); dialog.showErrorBox('VRCX', 'Failed to create desktop entry.');
}); return;
}
} }
function downloadIcon(url, targetPath) { function downloadIcon(url, targetPath) {
@@ -501,7 +498,7 @@ function getVersion() {
// look for trailing git hash "-22bcd96" to indicate nightly build // look for trailing git hash "-22bcd96" to indicate nightly build
var version = versionFile.split('-'); var version = versionFile.split('-');
console.log('Version:', version); console.log('Version:', versionFile);
if (version.length > 0 && version[version.length - 1].length == 7) { if (version.length > 0 && version[version.length - 1].length == 7) {
return `VRCX (Linux) Nightly ${versionFile}`; return `VRCX (Linux) Nightly ${versionFile}`;
} else { } else {
@@ -518,13 +515,9 @@ function isDotNetInstalled() {
// Assume .NET is already installed on macOS // Assume .NET is already installed on macOS
return true; return true;
} }
const result = require('child_process').spawnSync( const result = spawnSync('dotnet', ['--list-runtimes'], {
'dotnet', encoding: 'utf-8'
['--list-runtimes'], });
{
encoding: 'utf-8'
}
);
return result.stdout?.includes('.NETCore.App 9.0'); return result.stdout?.includes('.NETCore.App 9.0');
} }