diff --git a/AppApi.cs b/AppApi.cs
index 51d24f5f..654648d3 100644
--- a/AppApi.cs
+++ b/AppApi.cs
@@ -186,7 +186,7 @@ namespace VRCX
}
// TODO: fix this throwing an exception for being called before the browser is ready. somehow it gets past the checks
- if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading)
+ if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
MainForm.Instance.Browser.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning);
}
diff --git a/AutoAppLaunchManager.cs b/AutoAppLaunchManager.cs
index c6d410a9..10946a5d 100644
--- a/AutoAppLaunchManager.cs
+++ b/AutoAppLaunchManager.cs
@@ -77,6 +77,8 @@ namespace VRCX
if (KillChildrenOnExit)
KillChildProcesses();
+ else
+ UpdateChildProcesses();
}
private void OnProcessStarted(MonitoredProcess monitoredProcess)
@@ -86,6 +88,8 @@ namespace VRCX
if (KillChildrenOnExit)
KillChildProcesses();
+ else
+ UpdateChildProcesses();
var shortcutFiles = FindShortcutFiles(AppShortcutDirectory);
@@ -107,7 +111,7 @@ namespace VRCX
{
var process = pair.Value;
- if (!process.HasExited)
+ if (!WinApi.HasProcessExited(process.Id))
{
KillProcessTree(process.Id);
//process.Kill();
@@ -137,6 +141,7 @@ namespace VRCX
}
// Gonna be honest, not gonna spin up a 32bit windows VM to make sure this works. but it should.
+ // Does VRCX even run on 32bit windows?
PROCESSENTRY32 procEntry = new PROCESSENTRY32();
procEntry.dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32));
@@ -182,7 +187,7 @@ namespace VRCX
foreach (var pair in startedProcesses.ToList())
{
var process = pair.Value;
- if (process.HasExited)
+ if (WinApi.HasProcessExited(process.Id))
startedProcesses.Remove(pair.Key);
}
}
diff --git a/ProcessMonitor.cs b/ProcessMonitor.cs
index 1df87495..4547cc53 100644
--- a/ProcessMonitor.cs
+++ b/ProcessMonitor.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Timers;
namespace VRCX
@@ -72,7 +73,7 @@ namespace VRCX
if (monitoredProcess.IsRunning)
{
- if (monitoredProcess.Process == null || monitoredProcess.Process.HasExited)
+ if (monitoredProcess.Process == null || WinApi.HasProcessExited(monitoredProcess.Process.Id))
{
monitoredProcess.ProcessExited();
ProcessExited?.Invoke(monitoredProcess);
@@ -168,7 +169,7 @@ namespace VRCX
Process = process;
ProcessName = process.ProcessName.ToLower();
- if (!process.HasExited)
+ if (process != null && !WinApi.HasProcessExited(process.Id))
IsRunning = true;
}
diff --git a/WinApi.cs b/WinApi.cs
index 641948a1..fbfa967a 100644
--- a/WinApi.cs
+++ b/WinApi.cs
@@ -23,5 +23,45 @@ namespace VRCX
public static extern int SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut);
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr[] apidl, uint dwFlags);
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
+
+ ///
+ /// Flag that specifies the access rights to query limited information about a process.
+ /// This won't throw an exception when we try to access info about an elevated process
+ ///
+ private const int PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
+
+ ///
+ /// Determines whether the specified process has exited using WinAPI's GetExitCodeProcess running with PROCESS_QUERY_LIMITED_INFORMATION.
+ /// We do this because Process.HasExited in .net framework opens a handle with PROCESS_QUERY_INFORMATION, which will throw an exception if the process is elevated.
+ /// GetExitCodeProcess works with PROCESS_QUERY_LIMITED_INFORMATION, which will not throw an exception if the process is elevated.
+ /// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess
+ ///
+ /// The process to check.
+ /// true if the process has exited; otherwise, false.
+ internal static bool HasProcessExited(int processId)
+ {
+ IntPtr hProcess = WinApi.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, processId);
+ if (hProcess == IntPtr.Zero)
+ {
+ // this is probably fine
+ return true;
+ //throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
+ }
+
+ bool exited;
+ if (!WinApi.GetExitCodeProcess(hProcess, out uint exitCode))
+ {
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
+ }
+
+ // Fun fact, If a program uses STILL_ACTIVE (259) as an exit code, GetExitCodeProcess will return 259, since it returns... the exit code. This would break this function.
+ exited = exitCode != 259;
+ return exited;
+ }
}
}
diff --git a/WorldDBManager.cs b/WorldDBManager.cs
index 0a2948f9..6536f85f 100644
--- a/WorldDBManager.cs
+++ b/WorldDBManager.cs
@@ -29,11 +29,21 @@ namespace VRCX
listener.Prefixes.Add(url);
worldDB = new WorldDatabase(Path.Combine(Program.AppDataDirectory, "VRCX-WorldData.db"));
+
}
public async Task Start()
{
- listener.Start();
+ // typing this in vr gonna kms
+ try
+ {
+ listener.Start();
+ }
+ catch (HttpListenerException e)
+ {
+ logger.Error(e, "Failed to start HTTP listener. Is VRCX already running?");
+ return;
+ }
logger.Info("Listening for requests on {0}", listener.Prefixes.First());
while (true)
@@ -46,7 +56,7 @@ namespace VRCX
{
if (MainForm.Instance?.Browser == null || MainForm.Instance.Browser.IsLoading || !MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
{
- logger.Warn("Received a request to {0} while VRCX is still initializing the browser window. Responding with error 503.", request.Url);
+ logger.Error("Received a request to {0} while VRCX is still initializing the browser window. Responding with error 503.", request.Url);
responseData.Error = "VRCX not yet initialized. Try again in a moment.";
responseData.StatusCode = 503;
@@ -84,15 +94,15 @@ namespace VRCX
responseData = await HandleBulkDataRequest(context);
SendJsonResponse(context.Response, responseData);
break;
+ case "/vrcx/data/settings":
+ responseData = await HandleSetSettingsRequest(context);
+ SendJsonResponse(context.Response, responseData);
+ break;
case "/vrcx/status":
// Send a blank 200 response to indicate that the server is running.
context.Response.StatusCode = 200;
context.Response.Close();
break;
- case "/vrcx/data/settings":
- responseData = await HandleSetSettingsRequest(context);
- SendJsonResponse(context.Response, responseData);
- break;
default:
responseData.Error = "Invalid VRCX endpoint.";
responseData.StatusCode = 404;
@@ -108,7 +118,7 @@ namespace VRCX
}
catch (Exception ex)
{
- logger.Error(ex, $"Exception while processing the url '{request.Url}'.");
+ logger.Error(ex, $"Exception while processing a request to endpoint '{request.Url}'.");
responseData.Error = $"VRCX has encountered an exception while processing the url '{request.Url}': {ex.Message}";
responseData.StatusCode = 500;
@@ -212,6 +222,23 @@ namespace VRCX
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
}
+ var worldOverride = request.QueryString["world"];
+ if (worldOverride != null && worldId != worldOverride)
+ {
+ var allowed = worldDB.GetWorldAllowExternalRead(worldOverride);
+ if (!allowed)
+ {
+ return ConstructSuccessResponse(null, connectionKey);
+ }
+
+ var otherValue = worldDB.GetDataEntry(worldOverride, key);
+
+ logger.Debug("Serving a request for data with key '{0}' from world ID '{1}' requested by world ID '{2}' with connection key {3}.", key, worldOverride, worldId, connectionKey);
+
+ // This value is intended to be null if the key doesn't exist.
+ return ConstructSuccessResponse(otherValue?.Value, connectionKey);
+ }
+
var value = worldDB.GetDataEntry(worldId, key);
logger.Debug("Serving a request for data with key '{0}' from world ID '{1}' with connection key {2}.", key, worldId, connectionKey);
@@ -227,8 +254,6 @@ namespace VRCX
private async Task HandleAllDataRequest(HttpListenerContext context)
{
var request = context.Request;
-
-
var worldId = await GetCurrentWorldID();
if (!TryInitializeWorld(worldId, out string connectionKey))
@@ -236,6 +261,27 @@ namespace VRCX
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
}
+ var worldOverride = request.QueryString["world"];
+ if (worldOverride != null && worldId != worldOverride)
+ {
+ var allowed = worldDB.GetWorldAllowExternalRead(worldOverride);
+ if (!allowed)
+ {
+ return ConstructSuccessResponse(null, connectionKey);
+ }
+
+ var otherEntries = worldDB.GetAllDataEntries(worldOverride);
+
+ var otherData = new Dictionary();
+ foreach (var entry in otherEntries)
+ {
+ otherData.Add(entry.Key, entry.Value);
+ }
+
+ logger.Debug("Serving a request for all data ({0} entries) for world ID '{1}' requested by {2} with connection key {3}.", otherData.Count, worldOverride, worldId, connectionKey);
+ return ConstructSuccessResponse(JsonConvert.SerializeObject(otherData), connectionKey);
+ }
+
var entries = worldDB.GetAllDataEntries(worldId);
var data = new Dictionary();
@@ -272,6 +318,27 @@ namespace VRCX
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
}
+ var worldOverride = request.QueryString["world"];
+ if (worldOverride != null && worldId != worldOverride)
+ {
+ var allowed = worldDB.GetWorldAllowExternalRead(worldOverride);
+ if (!allowed)
+ {
+ return ConstructSuccessResponse(null, connectionKey);
+ }
+
+ var otherEntries = worldDB.GetAllDataEntries(worldOverride);
+
+ var otherData = new Dictionary();
+ foreach (var entry in otherEntries)
+ {
+ otherData.Add(entry.Key, entry.Value);
+ }
+
+ logger.Debug("Serving a request for all data ({0} entries) for world ID '{1}' requested by {2} with connection key {3}.", otherData.Count, worldOverride, worldId, connectionKey);
+ return ConstructSuccessResponse(JsonConvert.SerializeObject(otherData), connectionKey);
+ }
+
var values = worldDB.GetDataEntries(worldId, keyArray).ToList();
// Build a dictionary of key/value pairs to send back. If a key doesn't exist in the database, the key will be included in the response as requested but with a null value.
@@ -288,6 +355,12 @@ namespace VRCX
return ConstructSuccessResponse(JsonConvert.SerializeObject(data), connectionKey);
}
+ ///
+ /// Attempts to initialize a world with the given ID by generating a connection key and adding it to the world database if it does not already exist.
+ ///
+ /// The ID of the world to initialize.
+ /// The connection key generated for the world.
+ /// True if the world was successfully initialized, false otherwise.
private bool TryInitializeWorld(string worldId, out string connectionKey)
{
if (String.IsNullOrEmpty(worldId))
@@ -476,73 +549,94 @@ namespace VRCX
return;
}
- string requestType = request.RequestType.ToLower();
-
- switch (requestType)
+ try
{
- case "store":
- if (String.IsNullOrEmpty(request.Key))
- {
- logger.Warn("World {0} tried to store data with no key provided", worldId);
- this.lastError = "`key` is missing or null";
- return;
- }
+ string requestType = request.RequestType.ToLower();
+ switch (requestType)
+ {
+ case "store":
+ if (String.IsNullOrEmpty(request.Key))
+ {
+ logger.Warn("World {0} tried to store data with no key provided", worldId);
+ this.lastError = "`key` is missing or null";
+ return;
+ }
- if (String.IsNullOrEmpty(request.Value))
- {
- logger.Warn("World {0} tried to store data under key {1} with no value provided", worldId, request.Key);
- this.lastError = "`value` is missing or null";
- return;
- }
+ if (request.Key.Length > 255)
+ {
+ logger.Warn("World {0} tried to store data with a key that was too long ({1}/256 characters)", worldId, request.Key.Length);
+ this.lastError = "`key` is too long. Keep it below <256 characters.";
+ return;
+ }
- StoreWorldData(worldId, request.Key, request.Value);
- break;
- case "delete":
- if (String.IsNullOrEmpty(request.Key))
- {
- logger.Warn("World {0} tried to delete data with no key provided", worldId);
- this.lastError = "`key` is missing or null";
- return;
- }
+ if (String.IsNullOrEmpty(request.Value))
+ {
+ logger.Warn("World {0} tried to store data under key {1} with no value provided", worldId, request.Key);
+ this.lastError = "`value` is missing or null";
+ return;
+ }
- DeleteWorldData(worldId, request.Key);
- break;
- case "delete-all":
- logger.Info("World {0} requested to delete all data.", worldId);
- worldDB.DeleteAllDataEntriesForWorld(worldId);
- break;
- case "set-setting":
- if (String.IsNullOrEmpty(request.Key))
- {
- logger.Warn("World {0} tried to delete data with no key provided", worldId);
- this.lastError = "`key` is missing or null";
- return;
- }
+ StoreWorldData(worldId, request.Key, request.Value);
- if (String.IsNullOrEmpty(request.Value))
- {
- logger.Warn("World {0} tried to set settings with no value provided", worldId);
- this.lastError = "`value` is missing or null";
- return;
- }
+ break;
+ case "delete":
+ if (String.IsNullOrEmpty(request.Key))
+ {
+ logger.Warn("World {0} tried to delete data with no key provided", worldId);
+ this.lastError = "`key` is missing or null";
+ return;
+ }
- SetWorldProperty(worldId, request.Key, request.Value);
- break;
- default:
- logger.Warn("World {0} sent an invalid request type '{0}'", worldId, request.RequestType);
- this.lastError = "Invalid request type";
- // invalid request type
- return;
+
+ DeleteWorldData(worldId, request.Key);
+ break;
+ case "delete-all":
+
+ logger.Info("World {0} requested to delete all data.", worldId);
+
+
+ worldDB.DeleteAllDataEntriesForWorld(worldId);
+ worldDB.UpdateWorldDataSize(worldId, 0);
+ break;
+ case "set-setting":
+ if (String.IsNullOrEmpty(request.Key))
+ {
+ logger.Warn("World {0} tried to delete data with no key provided", worldId);
+ this.lastError = "`key` is missing or null";
+ return;
+ }
+
+ if (String.IsNullOrEmpty(request.Value))
+ {
+ logger.Warn("World {0} tried to set settings with no value provided", worldId);
+ this.lastError = "`value` is missing or null";
+ return;
+ }
+
+ SetWorldProperty(worldId, request.Key, request.Value);
+ break;
+ default:
+ logger.Warn("World {0} sent an invalid request type '{0}'", worldId, request.RequestType);
+ this.lastError = "Invalid request type";
+ // invalid request type
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to process world data request for world {0}", worldId);
+ logger.Error("Failed Request: {0}", json);
+ this.lastError = ex.Message;
+ return;
}
}
- private void LogWarning(string message, params object[] args)
- {
- logger.Warn("World {0} - " + message, args);
- this.lastError = String.Format(message, args);
- }
-
-
+ ///
+ /// Sets a property for a given world in the world database.
+ ///
+ /// The ID of the world to set the property for.
+ /// The key of the property to set.
+ /// The value to set the property to.
public void SetWorldProperty(string worldId, string key, string value)
{
switch (key)
@@ -562,6 +656,12 @@ namespace VRCX
}
}
+ ///
+ /// Stores a data entry for a given world in the world database.
+ ///
+ /// The ID of the world to store the data entry for.
+ /// The key of the data entry to store.
+ /// The value of the data entry to store.
public void StoreWorldData(string worldId, string key, string value)
{
// Get/calculate the old and new data sizes for this key/the world
@@ -585,6 +685,11 @@ namespace VRCX
logger.Debug("{0} : {1}", key, value);
}
+ ///
+ /// Deletes a data entry for a given world from the world database.
+ ///
+ /// The ID of the world to delete the data entry from.
+ /// The key of the data entry to delete.
public void DeleteWorldData(string worldId, string key)
{
int oldTotalDataSize = worldDB.GetWorldDataSize(worldId);
diff --git a/WorldDatabase.cs b/WorldDatabase.cs
index 2113c1f3..e6e746cd 100644
--- a/WorldDatabase.cs
+++ b/WorldDatabase.cs
@@ -164,6 +164,18 @@ END;";
sqlite.Execute("UPDATE worlds SET allow_external_read = ? WHERE world_id = ?", allowExternalRead, worldId);
}
+ ///
+ /// Gets the value of the allow_external_read field for the world with the specified ID from the database.
+ ///
+ /// The ID of the world to get the allow_external_read field for.
+ /// The value of the allow_external_read field for the specified world.
+ public bool GetWorldAllowExternalRead(string worldId)
+ {
+ var query = sqlite.Table().Where(w => w.WorldId == worldId).Select(w => w.AllowExternalRead);
+
+ return query.FirstOrDefault();
+ }
+
///
/// Adds a new world to the database.
///
@@ -266,7 +278,7 @@ END;";
/// An enumerable collection of all data entries for the world with the specified ID.
public IEnumerable GetAllDataEntries(string worldId)
{
- var query = sqlite.Table().Where(w => w.WorldId == worldId);
+ var query = sqlite.Table().Where(w => w.WorldId == worldId).Take(10000);
return query.ToList();
}
@@ -283,11 +295,20 @@ END;";
return query.FirstOrDefault();
}
+ ///
+ /// Deletes the data entry with the specified world ID and key from the database.
+ ///
+ /// The ID of the world to delete the data entry from.
+ /// The key of the data entry to delete.
public void DeleteDataEntry(string worldId, string key)
{
sqlite.Execute("DELETE FROM data WHERE world_id = ? AND key = ?", worldId, key);
}
+ ///
+ /// Deletes all data entries for the world with the specified ID from the database.
+ ///
+ /// The ID of the world to delete all data entries for.
public void DeleteAllDataEntriesForWorld(string worldId)
{
sqlite.Execute("DELETE FROM data WHERE world_id = ?", worldId);