Skip to content

Commit 57192d5

Browse files
author
Alexandru Macocian
committed
Experimental multi-launch support
1 parent 6af7268 commit 57192d5

File tree

9 files changed

+349
-14
lines changed

9 files changed

+349
-14
lines changed

Daybreak/Configuration/ExperimentalFeatures.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
{
33
public sealed class ExperimentalFeatures
44
{
5+
public bool MultiLaunchSupport { get; set; }
56
}
67
}

Daybreak/Configuration/ProjectConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Daybreak.Services.Configuration;
55
using Daybreak.Services.Credentials;
66
using Daybreak.Services.Logging;
7+
using Daybreak.Services.Mutex;
78
using Daybreak.Services.Screenshots;
89
using Daybreak.Services.Updater;
910
using Daybreak.Services.ViewManagement;
@@ -31,6 +32,7 @@ public static void RegisterServices(IServiceProducer serviceProducer)
3132
serviceProducer.RegisterSingleton<IConfigurationManager, ConfigurationManager>();
3233
serviceProducer.RegisterSingleton<IBloogumClient, BloogumClient>();
3334
serviceProducer.RegisterSingleton<IApplicationUpdater, ApplicationUpdater>();
35+
serviceProducer.RegisterSingleton<IMutexHandler, MutexHandler>();
3436
}
3537
public static void RegisterLifetimeServices(IApplicationLifetimeProducer applicationLifetimeProducer)
3638
{

Daybreak/Daybreak.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
1010
<LangVersion>preview</LangVersion>
1111
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
12-
<Version>0.3.1</Version>
12+
<Version>0.4.0</Version>
1313
</PropertyGroup>
1414

1515
<ItemGroup>

Daybreak/Services/ApplicationDetection/ApplicationDetector.cs

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
1-
using Daybreak.Services.Configuration;
1+
using Daybreak.Models;
2+
using Daybreak.Services.Configuration;
23
using Daybreak.Services.Credentials;
4+
using Daybreak.Services.Mutex;
5+
using Microsoft.Win32;
6+
using Pepa.Wpf.Utilities;
37
using System;
48
using System.Collections.Generic;
59
using System.Diagnostics;
610
using System.Extensions;
711
using System.IO;
812
using System.Linq;
13+
using System.Runtime.InteropServices;
914

1015
namespace Daybreak.Services.ApplicationDetection
1116
{
1217
public class ApplicationDetector : IApplicationDetector
1318
{
1419
private const string ToolboxProcessName = "GWToolbox";
1520
private const string ProcessName = "gw";
21+
private const string ArenaNetMutex = "AN-Mute";
1622

1723
private readonly IConfigurationManager configurationManager;
1824
private readonly ICredentialManager credentialManager;
25+
private readonly IMutexHandler mutexHandler;
1926

20-
public bool IsGuildwarsRunning => GuildwarsProcessDetected();
27+
public bool IsGuildwarsRunning => this.GuildwarsProcessDetected();
2128
public bool IsToolboxRunning => GuildwarsToolboxProcessDetected();
2229

2330
public ApplicationDetector(
2431
IConfigurationManager configurationManager,
25-
ICredentialManager credentialManager)
32+
ICredentialManager credentialManager,
33+
IMutexHandler mutexHandler)
2634
{
35+
this.mutexHandler = mutexHandler.ThrowIfNull(nameof(mutexHandler));
2736
this.credentialManager = credentialManager.ThrowIfNull(nameof(credentialManager));
2837
this.configurationManager = configurationManager.ThrowIfNull(nameof(configurationManager));
2938
}
3039

3140
public void LaunchGuildwars()
3241
{
3342
var configuration = this.configurationManager.GetConfiguration();
34-
var executable = configuration.GamePath;
35-
if (File.Exists(executable) is false)
36-
{
37-
throw new InvalidOperationException($"Guildwars executable doesn't exist at {executable}");
38-
}
39-
4043
if (string.IsNullOrEmpty(configuration.CharacterName))
4144
{
4245
throw new InvalidOperationException($"No character name set");
@@ -46,10 +49,12 @@ public void LaunchGuildwars()
4649
auth.Do(
4750
onSome: (credentials) =>
4851
{
49-
if (Process.Start(executable, new List<string> { "-email", credentials.Username, "-password", credentials.Password, "-character", configuration.CharacterName }) is null)
52+
if (configuration.ExperimentalFeatures.MultiLaunchSupport is true)
5053
{
51-
throw new InvalidOperationException($"Unable to launch {executable}");
54+
ClearGwLocks();
5255
}
56+
57+
LaunchGuildwarsProcess(credentials.Username, credentials.Password, configuration.CharacterName);
5358
},
5459
onNone: () =>
5560
{
@@ -72,11 +77,85 @@ public void LaunchGuildwarsToolbox()
7277
}
7378
}
7479

75-
private static bool GuildwarsProcessDetected()
80+
private void LaunchGuildwarsProcess(string email, SecureString password, string character)
7681
{
82+
var executable = this.configurationManager.GetConfiguration().GamePath;
83+
if (File.Exists(executable) is false)
84+
{
85+
throw new InvalidOperationException($"Guildwars executable doesn't exist at {executable}");
86+
}
87+
88+
if (Process.Start(executable, new List<string> { "-email", email, "-password", password, "-character", character }) is null)
89+
{
90+
throw new InvalidOperationException($"Unable to launch {executable}");
91+
}
92+
}
93+
94+
private bool GuildwarsProcessDetected()
95+
{
96+
if (this.configurationManager.GetConfiguration().ExperimentalFeatures.MultiLaunchSupport is true)
97+
{
98+
try
99+
{
100+
using var stream = File.OpenWrite(this.configurationManager.GetConfiguration().GamePath);
101+
return false;
102+
}
103+
catch
104+
{
105+
return true;
106+
}
107+
}
108+
77109
return Process.GetProcessesByName(ProcessName).FirstOrDefault() is not null;
78110
}
79111

112+
private void ClearGwLocks()
113+
{
114+
this.SetRegistryGuildwarsPath();
115+
foreach (var process in Process.GetProcessesByName(ProcessName))
116+
{
117+
this.mutexHandler.CloseMutex(process, ArenaNetMutex);
118+
}
119+
}
120+
121+
private void SetRegistryGuildwarsPath()
122+
{
123+
var gamePath = this.configurationManager.GetConfiguration().GamePath;
124+
var registryKey = GetGuildwarsRegistryKey(true);
125+
registryKey.SetValue("Path", gamePath);
126+
registryKey.SetValue("Src", gamePath);
127+
registryKey.Close();
128+
}
129+
130+
private RegistryKey GetGuildwarsRegistryKey(bool write)
131+
{
132+
var gwKey = Registry.CurrentUser.OpenSubKey("SOFTWARE")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
133+
if (gwKey is not null)
134+
{
135+
return gwKey;
136+
}
137+
138+
gwKey = Registry.CurrentUser.OpenSubKey("SOFTWARE")?.OpenSubKey("WOW6432Node")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
139+
if (gwKey is not null)
140+
{
141+
return gwKey;
142+
}
143+
144+
gwKey = Registry.LocalMachine.OpenSubKey("SOFTWARE")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
145+
if (gwKey is not null)
146+
{
147+
return gwKey;
148+
}
149+
150+
gwKey = Registry.LocalMachine.OpenSubKey("SOFTWARE")?.OpenSubKey("WOW6432Node")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
151+
if (gwKey is not null)
152+
{
153+
return gwKey;
154+
}
155+
156+
throw new InvalidOperationException("Could not find registry key for guildwars.");
157+
}
158+
80159
private static bool GuildwarsToolboxProcessDetected()
81160
{
82161
return Process.GetProcessesByName(ToolboxProcessName).FirstOrDefault() is not null;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Diagnostics;
2+
3+
namespace Daybreak.Services.Mutex
4+
{
5+
public interface IMutexHandler
6+
{
7+
void CloseMutex(Process process, string mutexName);
8+
}
9+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using Pepa.Wpf.Utilities;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Diagnostics;
5+
using System.Runtime.InteropServices;
6+
7+
namespace Daybreak.Services.Mutex
8+
{
9+
public sealed class MutexHandler : IMutexHandler
10+
{
11+
public void CloseMutex(Process process, string mutexName)
12+
{
13+
CloseHandle(process, mutexName);
14+
}
15+
16+
private static List<NativeMethods.SystemHandleInformation> GetHandles(Process targetProcess, IntPtr systemHandle)
17+
{
18+
var processHandles = new List<NativeMethods.SystemHandleInformation>();
19+
var basePointer = systemHandle.ToInt64();
20+
NativeMethods.SystemHandleInformation currentHandleInfo;
21+
for (int i = 0; i < Marshal.ReadInt32(systemHandle); i++)
22+
{
23+
var currentOffset = IntPtr.Size + i * Marshal.SizeOf(typeof(NativeMethods.SystemHandleInformation));
24+
currentHandleInfo = (NativeMethods.SystemHandleInformation)Marshal.PtrToStructure(new IntPtr(basePointer + currentOffset), typeof(NativeMethods.SystemHandleInformation));
25+
if (currentHandleInfo.OwnerPID == (uint)targetProcess.Id)
26+
{
27+
processHandles.Add(currentHandleInfo);
28+
}
29+
}
30+
31+
return processHandles;
32+
}
33+
34+
private static void CloseHandle(Process targetProcess, string handleName)
35+
{
36+
var systemHandles = GetAllHandles();
37+
if (systemHandles == IntPtr.Zero)
38+
{
39+
return;
40+
}
41+
42+
List<NativeMethods.SystemHandleInformation> processHandles = GetHandles(targetProcess, systemHandles);
43+
Marshal.FreeHGlobal(systemHandles);
44+
var processHandle = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.DupHandle, false, (uint)targetProcess.Id);
45+
foreach (var handleInfo in processHandles)
46+
{
47+
if (GetHandleName(handleInfo, processHandle).Contains(handleName))
48+
{
49+
if (CloseOwnedHandle(handleInfo.OwnerPID, new IntPtr(handleInfo.HandleValue)))
50+
{
51+
NativeMethods.CloseHandle(processHandle);
52+
return;
53+
}
54+
}
55+
}
56+
57+
NativeMethods.CloseHandle(processHandle);
58+
return;
59+
}
60+
61+
private static string GetHandleName(NativeMethods.SystemHandleInformation targetHandleInfo, IntPtr processHandle)
62+
{
63+
if (targetHandleInfo.AccessMask.ToInt64() == 0x0012019F)
64+
{
65+
return string.Empty;
66+
}
67+
68+
var thisProcess = Process.GetCurrentProcess().Handle;
69+
NativeMethods.DuplicateHandle(processHandle, new IntPtr(targetHandleInfo.HandleValue), thisProcess, out var handle, 0, false, NativeMethods.DuplicateOptions.DUPLICATE_SAME_ACCESS);
70+
var bufferSize = GetHandleNameLength(handle);
71+
var stringBuffer = Marshal.AllocHGlobal(bufferSize);
72+
NativeMethods.NtQueryObject(handle, NativeMethods.ObjectInformationClass.ObjectNameInformation, stringBuffer, bufferSize, out _);
73+
NativeMethods.CloseHandle(handle);
74+
var handleName = ConvertToString(stringBuffer);
75+
Marshal.FreeHGlobal(stringBuffer);
76+
return handleName;
77+
}
78+
79+
private static IntPtr GetAllHandles()
80+
{
81+
int bufferSize = 0x10000;
82+
var pSysInfoBuffer = Marshal.AllocHGlobal(bufferSize);
83+
var queryResult = NativeMethods.NtQuerySystemInformation(NativeMethods.SystemInformationClass.SystemHandleInformation,
84+
pSysInfoBuffer, bufferSize, out _);
85+
86+
while (queryResult == NativeMethods.NtStatus.STATUS_INFO_LENGTH_MISMATCH)
87+
{
88+
Marshal.FreeHGlobal(pSysInfoBuffer);
89+
bufferSize *= 2;
90+
pSysInfoBuffer = Marshal.AllocHGlobal(bufferSize);
91+
queryResult = NativeMethods.NtQuerySystemInformation(NativeMethods.SystemInformationClass.SystemHandleInformation,
92+
pSysInfoBuffer, bufferSize, out _);
93+
}
94+
95+
if (queryResult == NativeMethods.NtStatus.STATUS_SUCCESS)
96+
{
97+
return pSysInfoBuffer;
98+
}
99+
else
100+
{
101+
Marshal.FreeHGlobal(pSysInfoBuffer);
102+
return IntPtr.Zero;
103+
}
104+
}
105+
106+
private static int GetHandleNameLength(IntPtr handle)
107+
{
108+
var infoBufferSize = Marshal.SizeOf(typeof(NativeMethods.ObjectBasicInformation));
109+
var pInfoBuffer = Marshal.AllocHGlobal(infoBufferSize);
110+
NativeMethods.NtQueryObject(handle, NativeMethods.ObjectInformationClass.ObjectBasicInformation, pInfoBuffer, infoBufferSize, out _);
111+
NativeMethods.ObjectBasicInformation objInfo = (NativeMethods.ObjectBasicInformation)Marshal.PtrToStructure(pInfoBuffer, typeof(NativeMethods.ObjectBasicInformation));
112+
Marshal.FreeHGlobal(pInfoBuffer);
113+
if (objInfo.NameInformationLength == 0)
114+
{
115+
return 0x100;
116+
}
117+
else
118+
{
119+
return (int)objInfo.NameInformationLength;
120+
}
121+
}
122+
123+
private static string ConvertToString(IntPtr stringBuffer)
124+
{
125+
var baseAddress = stringBuffer.ToInt64();
126+
var offset = IntPtr.Size * 2;
127+
var handleName = Marshal.PtrToStringUni(new IntPtr(baseAddress + offset));
128+
return handleName;
129+
}
130+
131+
private static bool CloseOwnedHandle(uint processId, IntPtr handleToClose)
132+
{
133+
var processHandle = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.All, false, processId);
134+
var success = NativeMethods.DuplicateHandle(processHandle, handleToClose, IntPtr.Zero, out _, 0, false, NativeMethods.DuplicateOptions.DUPLICATE_CLOSE_SOURCE);
135+
NativeMethods.CloseHandle(processHandle);
136+
return success;
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)