Skip to content

Commit

Permalink
Experimental multi-launch support
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandru Macocian committed Apr 12, 2021
1 parent 6af7268 commit 57192d5
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 14 deletions.
1 change: 1 addition & 0 deletions Daybreak/Configuration/ExperimentalFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
{
public sealed class ExperimentalFeatures
{
public bool MultiLaunchSupport { get; set; }
}
}
2 changes: 2 additions & 0 deletions Daybreak/Configuration/ProjectConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Daybreak.Services.Configuration;
using Daybreak.Services.Credentials;
using Daybreak.Services.Logging;
using Daybreak.Services.Mutex;
using Daybreak.Services.Screenshots;
using Daybreak.Services.Updater;
using Daybreak.Services.ViewManagement;
Expand Down Expand Up @@ -31,6 +32,7 @@ public static void RegisterServices(IServiceProducer serviceProducer)
serviceProducer.RegisterSingleton<IConfigurationManager, ConfigurationManager>();
serviceProducer.RegisterSingleton<IBloogumClient, BloogumClient>();
serviceProducer.RegisterSingleton<IApplicationUpdater, ApplicationUpdater>();
serviceProducer.RegisterSingleton<IMutexHandler, MutexHandler>();
}
public static void RegisterLifetimeServices(IApplicationLifetimeProducer applicationLifetimeProducer)
{
Expand Down
2 changes: 1 addition & 1 deletion Daybreak/Daybreak.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<Version>0.3.1</Version>
<Version>0.4.0</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
103 changes: 91 additions & 12 deletions Daybreak/Services/ApplicationDetection/ApplicationDetector.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
using Daybreak.Services.Configuration;
using Daybreak.Models;
using Daybreak.Services.Configuration;
using Daybreak.Services.Credentials;
using Daybreak.Services.Mutex;
using Microsoft.Win32;
using Pepa.Wpf.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Extensions;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;

namespace Daybreak.Services.ApplicationDetection
{
public class ApplicationDetector : IApplicationDetector
{
private const string ToolboxProcessName = "GWToolbox";
private const string ProcessName = "gw";
private const string ArenaNetMutex = "AN-Mute";

private readonly IConfigurationManager configurationManager;
private readonly ICredentialManager credentialManager;
private readonly IMutexHandler mutexHandler;

public bool IsGuildwarsRunning => GuildwarsProcessDetected();
public bool IsGuildwarsRunning => this.GuildwarsProcessDetected();
public bool IsToolboxRunning => GuildwarsToolboxProcessDetected();

public ApplicationDetector(
IConfigurationManager configurationManager,
ICredentialManager credentialManager)
ICredentialManager credentialManager,
IMutexHandler mutexHandler)
{
this.mutexHandler = mutexHandler.ThrowIfNull(nameof(mutexHandler));
this.credentialManager = credentialManager.ThrowIfNull(nameof(credentialManager));
this.configurationManager = configurationManager.ThrowIfNull(nameof(configurationManager));
}

public void LaunchGuildwars()
{
var configuration = this.configurationManager.GetConfiguration();
var executable = configuration.GamePath;
if (File.Exists(executable) is false)
{
throw new InvalidOperationException($"Guildwars executable doesn't exist at {executable}");
}

if (string.IsNullOrEmpty(configuration.CharacterName))
{
throw new InvalidOperationException($"No character name set");
Expand All @@ -46,10 +49,12 @@ public void LaunchGuildwars()
auth.Do(
onSome: (credentials) =>
{
if (Process.Start(executable, new List<string> { "-email", credentials.Username, "-password", credentials.Password, "-character", configuration.CharacterName }) is null)
if (configuration.ExperimentalFeatures.MultiLaunchSupport is true)
{
throw new InvalidOperationException($"Unable to launch {executable}");
ClearGwLocks();
}

LaunchGuildwarsProcess(credentials.Username, credentials.Password, configuration.CharacterName);
},
onNone: () =>
{
Expand All @@ -72,11 +77,85 @@ public void LaunchGuildwarsToolbox()
}
}

private static bool GuildwarsProcessDetected()
private void LaunchGuildwarsProcess(string email, SecureString password, string character)
{
var executable = this.configurationManager.GetConfiguration().GamePath;
if (File.Exists(executable) is false)
{
throw new InvalidOperationException($"Guildwars executable doesn't exist at {executable}");
}

if (Process.Start(executable, new List<string> { "-email", email, "-password", password, "-character", character }) is null)
{
throw new InvalidOperationException($"Unable to launch {executable}");
}
}

private bool GuildwarsProcessDetected()
{
if (this.configurationManager.GetConfiguration().ExperimentalFeatures.MultiLaunchSupport is true)
{
try
{
using var stream = File.OpenWrite(this.configurationManager.GetConfiguration().GamePath);
return false;
}
catch
{
return true;
}
}

return Process.GetProcessesByName(ProcessName).FirstOrDefault() is not null;
}

private void ClearGwLocks()
{
this.SetRegistryGuildwarsPath();
foreach (var process in Process.GetProcessesByName(ProcessName))
{
this.mutexHandler.CloseMutex(process, ArenaNetMutex);
}
}

private void SetRegistryGuildwarsPath()
{
var gamePath = this.configurationManager.GetConfiguration().GamePath;
var registryKey = GetGuildwarsRegistryKey(true);
registryKey.SetValue("Path", gamePath);
registryKey.SetValue("Src", gamePath);
registryKey.Close();
}

private RegistryKey GetGuildwarsRegistryKey(bool write)
{
var gwKey = Registry.CurrentUser.OpenSubKey("SOFTWARE")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
if (gwKey is not null)
{
return gwKey;
}

gwKey = Registry.CurrentUser.OpenSubKey("SOFTWARE")?.OpenSubKey("WOW6432Node")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
if (gwKey is not null)
{
return gwKey;
}

gwKey = Registry.LocalMachine.OpenSubKey("SOFTWARE")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
if (gwKey is not null)
{
return gwKey;
}

gwKey = Registry.LocalMachine.OpenSubKey("SOFTWARE")?.OpenSubKey("WOW6432Node")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
if (gwKey is not null)
{
return gwKey;
}

throw new InvalidOperationException("Could not find registry key for guildwars.");
}

private static bool GuildwarsToolboxProcessDetected()
{
return Process.GetProcessesByName(ToolboxProcessName).FirstOrDefault() is not null;
Expand Down
9 changes: 9 additions & 0 deletions Daybreak/Services/Mutex/IMutexHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Diagnostics;

namespace Daybreak.Services.Mutex
{
public interface IMutexHandler
{
void CloseMutex(Process process, string mutexName);
}
}
139 changes: 139 additions & 0 deletions Daybreak/Services/Mutex/MutexHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using Pepa.Wpf.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Daybreak.Services.Mutex
{
public sealed class MutexHandler : IMutexHandler
{
public void CloseMutex(Process process, string mutexName)
{
CloseHandle(process, mutexName);
}

private static List<NativeMethods.SystemHandleInformation> GetHandles(Process targetProcess, IntPtr systemHandle)
{
var processHandles = new List<NativeMethods.SystemHandleInformation>();
var basePointer = systemHandle.ToInt64();
NativeMethods.SystemHandleInformation currentHandleInfo;
for (int i = 0; i < Marshal.ReadInt32(systemHandle); i++)
{
var currentOffset = IntPtr.Size + i * Marshal.SizeOf(typeof(NativeMethods.SystemHandleInformation));
currentHandleInfo = (NativeMethods.SystemHandleInformation)Marshal.PtrToStructure(new IntPtr(basePointer + currentOffset), typeof(NativeMethods.SystemHandleInformation));
if (currentHandleInfo.OwnerPID == (uint)targetProcess.Id)
{
processHandles.Add(currentHandleInfo);
}
}

return processHandles;
}

private static void CloseHandle(Process targetProcess, string handleName)
{
var systemHandles = GetAllHandles();
if (systemHandles == IntPtr.Zero)
{
return;
}

List<NativeMethods.SystemHandleInformation> processHandles = GetHandles(targetProcess, systemHandles);
Marshal.FreeHGlobal(systemHandles);
var processHandle = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.DupHandle, false, (uint)targetProcess.Id);
foreach (var handleInfo in processHandles)
{
if (GetHandleName(handleInfo, processHandle).Contains(handleName))
{
if (CloseOwnedHandle(handleInfo.OwnerPID, new IntPtr(handleInfo.HandleValue)))
{
NativeMethods.CloseHandle(processHandle);
return;
}
}
}

NativeMethods.CloseHandle(processHandle);
return;
}

private static string GetHandleName(NativeMethods.SystemHandleInformation targetHandleInfo, IntPtr processHandle)
{
if (targetHandleInfo.AccessMask.ToInt64() == 0x0012019F)
{
return string.Empty;
}

var thisProcess = Process.GetCurrentProcess().Handle;
NativeMethods.DuplicateHandle(processHandle, new IntPtr(targetHandleInfo.HandleValue), thisProcess, out var handle, 0, false, NativeMethods.DuplicateOptions.DUPLICATE_SAME_ACCESS);
var bufferSize = GetHandleNameLength(handle);
var stringBuffer = Marshal.AllocHGlobal(bufferSize);
NativeMethods.NtQueryObject(handle, NativeMethods.ObjectInformationClass.ObjectNameInformation, stringBuffer, bufferSize, out _);
NativeMethods.CloseHandle(handle);
var handleName = ConvertToString(stringBuffer);
Marshal.FreeHGlobal(stringBuffer);
return handleName;
}

private static IntPtr GetAllHandles()
{
int bufferSize = 0x10000;
var pSysInfoBuffer = Marshal.AllocHGlobal(bufferSize);
var queryResult = NativeMethods.NtQuerySystemInformation(NativeMethods.SystemInformationClass.SystemHandleInformation,
pSysInfoBuffer, bufferSize, out _);

while (queryResult == NativeMethods.NtStatus.STATUS_INFO_LENGTH_MISMATCH)
{
Marshal.FreeHGlobal(pSysInfoBuffer);
bufferSize *= 2;
pSysInfoBuffer = Marshal.AllocHGlobal(bufferSize);
queryResult = NativeMethods.NtQuerySystemInformation(NativeMethods.SystemInformationClass.SystemHandleInformation,
pSysInfoBuffer, bufferSize, out _);
}

if (queryResult == NativeMethods.NtStatus.STATUS_SUCCESS)
{
return pSysInfoBuffer;
}
else
{
Marshal.FreeHGlobal(pSysInfoBuffer);
return IntPtr.Zero;
}
}

private static int GetHandleNameLength(IntPtr handle)
{
var infoBufferSize = Marshal.SizeOf(typeof(NativeMethods.ObjectBasicInformation));
var pInfoBuffer = Marshal.AllocHGlobal(infoBufferSize);
NativeMethods.NtQueryObject(handle, NativeMethods.ObjectInformationClass.ObjectBasicInformation, pInfoBuffer, infoBufferSize, out _);
NativeMethods.ObjectBasicInformation objInfo = (NativeMethods.ObjectBasicInformation)Marshal.PtrToStructure(pInfoBuffer, typeof(NativeMethods.ObjectBasicInformation));
Marshal.FreeHGlobal(pInfoBuffer);
if (objInfo.NameInformationLength == 0)
{
return 0x100;
}
else
{
return (int)objInfo.NameInformationLength;
}
}

private static string ConvertToString(IntPtr stringBuffer)
{
var baseAddress = stringBuffer.ToInt64();
var offset = IntPtr.Size * 2;
var handleName = Marshal.PtrToStringUni(new IntPtr(baseAddress + offset));
return handleName;
}

private static bool CloseOwnedHandle(uint processId, IntPtr handleToClose)
{
var processHandle = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.All, false, processId);
var success = NativeMethods.DuplicateHandle(processHandle, handleToClose, IntPtr.Zero, out _, 0, false, NativeMethods.DuplicateOptions.DUPLICATE_CLOSE_SOURCE);
NativeMethods.CloseHandle(processHandle);
return success;
}
}
}
Loading

0 comments on commit 57192d5

Please sign in to comment.