diff --git a/AppApi.cs b/AppApi.cs index 24b37ea5..fdc19852 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -337,6 +337,13 @@ namespace VRCX VRCXVR._browser2.ExecuteScriptAsync($"$app.{function}", json); } + public string GetLaunchCommand() + { + string command = StartupArgs.LaunchCommand; + StartupArgs.LaunchCommand = string.Empty; + return command; + } + public void SetStartup(bool enabled) { try diff --git a/IPCClient.cs b/IPCClient.cs new file mode 100644 index 00000000..9904a831 --- /dev/null +++ b/IPCClient.cs @@ -0,0 +1,73 @@ +// Copyright(c) 2019-2021 pypy. All rights reserved. +// +// This work is licensed under the terms of the MIT license. +// For a copy, see . + +using CefSharp; +using System; +using System.IO.Pipes; +using System.Text; + +namespace VRCX +{ + class IPCClient + { + private NamedPipeServerStream _ipcServer; + private byte[] _recvBuffer = new byte[1024 * 8]; + + private string _currentPacket; + + public IPCClient(NamedPipeServerStream ipcServer) + { + _ipcServer = ipcServer; + } + + public void BeginRead() + { + _ipcServer.BeginRead(_recvBuffer, 0, _recvBuffer.Length, OnRead, _ipcServer); + } + + private void OnRead(IAsyncResult asyncResult) + { + try + { + var bytesRead = _ipcServer.EndRead(asyncResult); + + if (bytesRead <= 0) + { + _ipcServer.Close(); + return; + } + + _currentPacket += Encoding.UTF8.GetString(_recvBuffer, 0, bytesRead); + + if (_currentPacket[_currentPacket.Length - 1] == (char)0x00) + { + var packets = _currentPacket.Split((char)0x00); + + foreach (var packet in packets) + { + if (string.IsNullOrEmpty(packet)) + continue; + + MainForm.Instance.Browser.ExecuteScriptAsync("$app.ipcEvent", packet); + } + + _currentPacket = string.Empty; + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + + BeginRead(); + } + + public static void OnSend(IAsyncResult asyncResult) + { + var ipcClient = (NamedPipeClientStream)asyncResult.AsyncState; + ipcClient.EndWrite(asyncResult); + } + } +} diff --git a/IPCServer.cs b/IPCServer.cs new file mode 100644 index 00000000..6b03b427 --- /dev/null +++ b/IPCServer.cs @@ -0,0 +1,48 @@ +// Copyright(c) 2019-2021 pypy. All rights reserved. +// +// This work is licensed under the terms of the MIT license. +// For a copy, see . + +using System; +using System.IO.Pipes; + +namespace VRCX +{ + class IPCServer + { + public static readonly IPCServer Instance; + + static IPCServer() + { + Instance = new IPCServer(); + } + + public void Init() + { + new IPCServer().CreateIPCServer(); + } + + public void CreateIPCServer() + { + var ipcServer = new NamedPipeServerStream("vrcx-ipc", PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + ipcServer.BeginWaitForConnection(asyncResult => DoAccept(asyncResult), ipcServer); + } + + private void DoAccept(IAsyncResult asyncResult) + { + var ipcServer = (NamedPipeServerStream)asyncResult.AsyncState; + + try + { + ipcServer.EndWaitForConnection(asyncResult); + } + catch (Exception e) + { + Console.WriteLine(e); + } + + new IPCClient(ipcServer).BeginRead(); + CreateIPCServer(); + } + } +} diff --git a/Installer/installer.nsi b/Installer/installer.nsi index aa9f0d68..b1f82c75 100644 --- a/Installer/installer.nsi +++ b/Installer/installer.nsi @@ -145,6 +145,14 @@ Section "Install" SecInstall CreateShortCut "$SMPROGRAMS\VRCX.lnk" "$INSTDIR\VRCX.exe" ApplicationID::Set "$SMPROGRAMS\VRCX.lnk" "VRCX" + WriteRegStr HKCU "Software\Classes\vrcx" "" "URL:vrcx" + WriteRegStr HKCU "Software\Classes\vrcx" "FriendlyTypeName" "VRCX" + WriteRegStr HKCU "Software\Classes\vrcx" "URL Protocol" "" + WriteRegExpandStr HKCU "Software\Classes\vrcx\DefaultIcon" "" "$INSTDIR\VRCX.ico" + WriteRegStr HKCU "Software\Classes\vrcx\shell" "" "open" + WriteRegStr HKCU "Software\Classes\vrcx\shell\open" "FriendlyAppName" "VRCX" + WriteRegStr HKCU "Software\Classes\vrcx\shell\open\command" "" '"D:\WindowsFiles\Documents\git\VRCX\bin\x64\Release\VRCX.exe" /uri="%1" /params="%2 %3 %4"' + ${If} ${Silent} SetOutPath $INSTDIR ShellExecAsUser::ShellExecAsUser "" "$INSTDIR\VRCX.exe" "" @@ -169,6 +177,7 @@ Section "Uninstall" DeleteRegKey HKLM "Software\VRCX" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\VRCX" + DeleteRegKey HKCU "Software\Classes\vrcx" Delete "$SMPROGRAMS\VRCX.lnk" diff --git a/Program.cs b/Program.cs index ce912213..59fcaa31 100644 --- a/Program.cs +++ b/Program.cs @@ -53,6 +53,7 @@ namespace VRCX private static void Run() { + StartupArgs.ArgsCheck(); Update.Check(); Application.EnableVisualStyles(); @@ -66,6 +67,7 @@ namespace VRCX LogWatcher.Instance.Init(); CefService.Instance.Init(); + IPCServer.Instance.Init(); VRCXVR.Instance.Init(); Application.Run(new MainForm()); WebApi.Instance.SaveCookies(); diff --git a/StartupArgs.cs b/StartupArgs.cs new file mode 100644 index 00000000..11d85302 --- /dev/null +++ b/StartupArgs.cs @@ -0,0 +1,72 @@ +// Copyright(c) 2019-2021 pypy. All rights reserved. +// +// This work is licensed under the terms of the MIT license. +// For a copy, see . + +using System; +using System.Diagnostics; +using System.IO.Pipes; +using System.Runtime.InteropServices; +using System.Text; + +namespace VRCX +{ + class StartupArgs + { + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ShowWindow(IntPtr hWnd, int flags); + + [DllImport("user32.dll")] + private static extern int SetForegroundWindow(IntPtr hwnd); + + public static string LaunchCommand; + + public static void ArgsCheck() + { + string[] args = Environment.GetCommandLineArgs(); + if (args.Length == 0) + return; + + foreach (string arg in args) + { + if (arg.Length > 12 && arg.Substring(0, 12) == "/uri=vrcx://") + { + LaunchCommand = arg.Substring(12); + ProcessCheck(LaunchCommand); + } + } + } + + private static void ProcessCheck(string command) + { + Process currentProcess = Process.GetCurrentProcess(); + Process[] processList = Process.GetProcessesByName("VRCX"); + + if (processList.Length > 1) + { + new IPCServer().CreateIPCServer(); + var ipcClient = new NamedPipeClientStream(".", "vrcx-ipc", PipeDirection.InOut); + ipcClient.Connect(); + + if (ipcClient.IsConnected) + { + var buffer = Encoding.UTF8.GetBytes($"{{\"type\":\"LaunchCommand\",\"command\":\"{command}\"}}" + (char)0x00); + ipcClient.BeginWrite(buffer, 0, buffer.Length, IPCClient.OnSend, ipcClient); + } + + foreach (Process process in processList) + { + if (process.Id != currentProcess.Id) + { + IntPtr handle = process.MainWindowHandle; + ShowWindow(handle, 9); + SetForegroundWindow(handle); + } + } + + Environment.Exit(0); + } + } + } +} diff --git a/VRCX.csproj b/VRCX.csproj index 85c8c53f..9176f3ad 100644 --- a/VRCX.csproj +++ b/VRCX.csproj @@ -81,6 +81,9 @@ + + + diff --git a/html/src/app.js b/html/src/app.js index bd40ba61..09109957 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -15582,6 +15582,53 @@ speechSynthesis.getVoices(); return user.currentAvatarImageUrl; }; + $app.methods.ipcEvent = function (json) { + try { + var data = JSON.parse(json); + } catch { + console.error(`IPC invalid JSON, ${json}`); + } + switch (data.type) { + case 'Ping': + this.eventPing(data); + break; + case 'LaunchCommand': + this.eventLaunchCommand(data.command); + break; + } + }; + + API.$on('LOGIN', async function () { + var command = await AppApi.GetLaunchCommand(); + if (command) { + $app.eventLaunchCommand(command); + } + }); + + $app.methods.eventLaunchCommand = function (command) { + if (!API.isLoggedIn) { + return; + } + var args = command.split('/'); + var command = args[0]; + var commandArg = args[1]; + switch (command) { + case 'world': + this.showWorldDialog(commandArg); + break; + case 'avatar': + this.showAvatarDialog(commandArg); + break; + case 'user': + this.showUserDialog(commandArg); + break; + } + }; + + $app.methods.eventPing = function (data) { + console.log('IPC Ping', data.version); + }; + $app = new Vue($app); window.$app = $app; })();