FFXIVLauncher-Netmaui
673 строки · 26.1 Кб
1using Microsoft.Win32;
2using Microsoft.Win32.SafeHandles;
3using Newtonsoft.Json;
4using Steamworks;
5using System;
6using System.Collections.Generic;
7using System.ComponentModel;
8using System.Diagnostics;
9using System.Linq;
10using System.Reflection;
11using System.Runtime.InteropServices;
12using System.Runtime.InteropServices.ComTypes;
13using System.Text;
14using System.Threading.Tasks;
15using XIVLauncher.Common.PlatformAbstractions;
16
17namespace LibDalamud.Common.Dalamud
18{
19public class WindowsDalamudRunner : IDalamudRunner
20{
21public Process? Run(FileInfo runner, bool fakeLogin, bool noPlugins, bool noThirdPlugins, FileInfo gameExe, string gameArgs, IDictionary<string, string> environment, DalamudLoadMethod loadMethod, DalamudStartInfo startInfo)
22{
23var inheritableCurrentProcess = GetInheritableCurrentProcessHandle();
24
25var launchArguments = new List<string>
26{
27"launch",
28$"--mode={(loadMethod == DalamudLoadMethod.EntryPoint ? "entrypoint" : "inject")}",
29$"--handle-owner={(long)inheritableCurrentProcess.Handle}",
30$"--game=\"{gameExe.FullName}\"",
31$"--dalamud-working-directory=\"{startInfo.WorkingDirectory}\"",
32$"--dalamud-configuration-path=\"{startInfo.ConfigurationPath}\"",
33$"--dalamud-plugin-directory=\"{startInfo.PluginDirectory}\"",
34$"--dalamud-dev-plugin-directory=\"{startInfo.DefaultPluginDirectory}\"",
35$"--dalamud-asset-directory=\"{startInfo.AssetDirectory}\"",
36$"--dalamud-client-language={(int)startInfo.Language}",
37$"--dalamud-delay-initialize={startInfo.DelayInitializeMs}",
38$"--dalamud-tspack-b64={Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(startInfo.TroubleshootingPackData))}",
39};
40
41if (loadMethod == DalamudLoadMethod.ACLonly)
42launchArguments.Add("--without-dalamud");
43
44if (fakeLogin)
45launchArguments.Add("--fake-arguments");
46
47if (noPlugins)
48launchArguments.Add("--no-plugin");
49
50if (noThirdPlugins)
51launchArguments.Add("--no-3rd-plugin");
52
53launchArguments.Add("--");
54launchArguments.Add(gameArgs);
55
56var psi = new ProcessStartInfo(runner.FullName)
57{
58Arguments = string.Join(" ", launchArguments),
59RedirectStandardOutput = true,
60UseShellExecute = false,
61CreateNoWindow = true
62};
63
64foreach (var keyValuePair in environment)
65{
66if (psi.EnvironmentVariables.ContainsKey(keyValuePair.Key))
67psi.EnvironmentVariables[keyValuePair.Key] = keyValuePair.Value;
68else
69psi.EnvironmentVariables.Add(keyValuePair.Key, keyValuePair.Value);
70}
71
72try
73{
74var dalamudProcess = Process.Start(psi);
75var output = dalamudProcess.StandardOutput.ReadLine();
76
77if (output == null)
78throw new DalamudRunnerException("An internal Dalamud error has occured");
79
80try
81{
82var dalamudConsoleOutput = JsonConvert.DeserializeObject<DalamudConsoleOutput>(output);
83Process gameProcess;
84
85if (dalamudConsoleOutput.Handle == 0)
86{
87Console.WriteLine($"Dalamud returned NULL process handle, attempting to recover by creating a new one from pid {dalamudConsoleOutput.Pid}...");
88gameProcess = Process.GetProcessById(dalamudConsoleOutput.Pid);
89}
90else
91{
92gameProcess = new ExistingProcess((IntPtr)dalamudConsoleOutput.Handle);
93}
94
95try
96{
97Console.WriteLine($"Got game process handle {gameProcess.Handle} with pid {gameProcess.Id}");
98}
99catch (InvalidOperationException ex)
100{
101Console.WriteLine(ex.Message, $"Dalamud returned invalid process handle {gameProcess.Handle}, attempting to recover by creating a new one from pid {dalamudConsoleOutput.Pid}...");
102gameProcess = Process.GetProcessById(dalamudConsoleOutput.Pid);
103Console.WriteLine($"Recovered with process handle {gameProcess.Handle}");
104}
105
106if (gameProcess.Id != dalamudConsoleOutput.Pid)
107Console.WriteLine($"Internal Process ID {gameProcess.Id} does not match Dalamud provided one {dalamudConsoleOutput.Pid}");
108
109return gameProcess;
110}
111catch (JsonReaderException ex)
112{
113Console.WriteLine(ex.Message, $"Couldn't parse Dalamud output: {output}");
114return null;
115}
116}
117catch (Exception ex)
118{
119throw new DalamudRunnerException("Error trying to start Dalamud.", ex);
120}
121}
122
123/// <summary>
124/// DUPLICATE_* values for DuplicateHandle's dwDesiredAccess.
125/// </summary>
126[Flags]
127private enum DuplicateOptions : uint
128{
129/// <summary>
130/// Closes the source handle. This occurs regardless of any error status returned.
131/// </summary>
132CloseSource = 0x00000001,
133
134/// <summary>
135/// Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle.
136/// </summary>
137SameAccess = 0x00000002,
138}
139
140/// <summary>
141/// Duplicates an object handle.
142/// </summary>
143/// <param name="hSourceProcessHandle">
144/// A handle to the process with the handle to be duplicated.
145///
146/// The handle must have the PROCESS_DUP_HANDLE access right.
147/// </param>
148/// <param name="hSourceHandle">
149/// The handle to be duplicated. This is an open object handle that is valid in the context of the source process.
150/// For a list of objects whose handles can be duplicated, see the following Remarks section.
151/// </param>
152/// <param name="hTargetProcessHandle">
153/// A handle to the process that is to receive the duplicated handle.
154///
155/// The handle must have the PROCESS_DUP_HANDLE access right.
156/// </param>
157/// <param name="lpTargetHandle">
158/// A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process.
159///
160/// If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively.
161///
162/// If lpTargetHandle is NULL, the function duplicates the handle, but does not return the duplicate handle value to the caller. This behavior exists only for backward compatibility with previous versions of this function. You should not use this feature, as you will lose system resources until the target process terminates.
163///
164/// This parameter is ignored if hTargetProcessHandle is NULL.
165/// </param>
166/// <param name="dwDesiredAccess">
167/// The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section.
168///
169/// This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag. Otherwise, the flags that can be specified depend on the type of object whose handle is to be duplicated.
170///
171/// This parameter is ignored if hTargetProcessHandle is NULL.
172/// </param>
173/// <param name="bInheritHandle">
174/// A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited.
175///
176/// This parameter is ignored if hTargetProcessHandle is NULL.
177/// </param>
178/// <param name="dwOptions">
179/// Optional actions.
180/// </param>
181/// <returns>
182/// If the function succeeds, the return value is nonzero.
183///
184/// If the function fails, the return value is zero. To get extended error information, call GetLastError.
185/// </returns>
186/// <remarks>
187/// See https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle.
188/// </remarks>
189[DllImport("kernel32.dll", SetLastError = true)]
190[return: MarshalAs(UnmanagedType.Bool)]
191private static extern bool DuplicateHandle(
192IntPtr hSourceProcessHandle,
193IntPtr hSourceHandle,
194IntPtr hTargetProcessHandle,
195out IntPtr lpTargetHandle,
196uint dwDesiredAccess,
197[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
198DuplicateOptions dwOptions);
199
200private static Process GetInheritableCurrentProcessHandle()
201{
202if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess))
203{
204Console.WriteLine("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
205return null;
206}
207
208return new ExistingProcess(inheritableCurrentProcessHandle);
209}
210}
211public class ExistingProcess : Process
212{
213public ExistingProcess(IntPtr handle)
214{
215SetHandle(handle);
216}
217
218private void SetHandle(IntPtr handle)
219{
220var baseType = GetType().BaseType;
221if (baseType == null)
222return;
223
224var setProcessHandleMethod = baseType.GetMethod("SetProcessHandle",
225BindingFlags.NonPublic | BindingFlags.Instance);
226setProcessHandleMethod?.Invoke(this, new object[] { new SafeProcessHandle(handle, true) });
227}
228}
229public class WindowsDalamudCompatibilityCheck : IDalamudCompatibilityCheck
230{
231public void EnsureCompatibility()
232{
233if (!CheckVcRedists())
234throw new IDalamudCompatibilityCheck.NoRedistsException();
235
236EnsureArchitecture();
237}
238
239private static void EnsureArchitecture()
240{
241var arch = RuntimeInformation.ProcessArchitecture;
242
243switch (arch)
244{
245case Architecture.X86:
246throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("Dalamud is not supported on x86 architecture.");
247
248case Architecture.X64:
249break;
250
251case Architecture.Arm:
252throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("Dalamud is not supported on ARM32.");
253
254case Architecture.Arm64:
255throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("x64 emulation was not detected. Please make sure to run XIVLauncher with x64 emulation.");
256}
257}
258
259[DllImport("kernel32", SetLastError = true)]
260private static extern IntPtr LoadLibrary(string lpFileName);
261
262private static bool CheckLibrary(string fileName)
263{
264if (LoadLibrary(fileName) != IntPtr.Zero)
265{
266Console.WriteLine("Found " + fileName);
267return true;
268}
269else
270{
271Console.WriteLine("Could not find " + fileName);
272}
273return false;
274}
275
276private static bool CheckVcRedists()
277{
278// snipped from https://stackoverflow.com/questions/12206314/detect-if-visual-c-redistributable-for-visual-studio-2012-is-installed
279// and https://github.com/bitbeans/RedistributableChecker
280
281var vc2022Paths = new List<string>
282{
283@"SOFTWARE\Microsoft\DevDiv\VC\Servicing\14.0\RuntimeMinimum",
284@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64",
285@"SOFTWARE\Classes\Installer\Dependencies\Microsoft.VS.VC_RuntimeMinimumVSU_amd64,v14",
286@"SOFTWARE\Classes\Installer\Dependencies\VC,redist.x64,amd64,14.31,bundle",
287@"SOFTWARE\Classes\Installer\Dependencies\VC,redist.x64,amd64,14.30,bundle",
288@"SOFTWARE\Classes\Installer\Dependencies\VC,redist.x64,amd64,14.29,bundle",
289@"SOFTWARE\Classes\Installer\Dependencies\VC,redist.x64,amd64,14.28,bundle",
290// technically, this was introduced in VCrun2017 with 14.16
291// but we shouldn't go that far
292// here's a legacy vcrun2017 check
293@"Installer\Dependencies\,,amd64,14.0,bundle",
294// here's one for vcrun2015
295@"SOFTWARE\Classes\Installer\Dependencies\{d992c12e-cab2-426f-bde3-fb8c53950b0d}"
296};
297
298var dllPaths = new List<string>
299{
300"ucrtbase_clr0400",
301"vcruntime140_clr0400",
302"vcruntime140"
303};
304
305var passedRegistry = false;
306var passedDllChecks = true;
307
308foreach (var path in vc2022Paths)
309{
310Console.WriteLine("Checking Registry key: " + path);
311var vcregcheck = Registry.LocalMachine.OpenSubKey(path, false);
312if (vcregcheck == null) continue;
313
314var vcVersioncheck = vcregcheck.GetValue("Version") ?? "";
315
316if (((string)vcVersioncheck).StartsWith("14", StringComparison.Ordinal))
317{
318passedRegistry = true;
319Console.WriteLine("Passed Registry Check with: " + path);
320break;
321}
322}
323
324foreach (var path in dllPaths)
325{
326Console.WriteLine("Checking for DLL: " + path);
327passedDllChecks = passedDllChecks && CheckLibrary(path);
328}
329
330// Display our findings
331if (!passedRegistry)
332{
333Console.WriteLine("Failed all registry checks to find any Visual C++ 2015-2022 Runtimes.");
334}
335
336if (!passedDllChecks)
337{
338Console.WriteLine("Missing DLL files required by Dalamud.");
339}
340
341return (passedRegistry && passedDllChecks);
342}
343}
344public class WindowsRestartManager : IDisposable
345{
346public delegate void RmWriteStatusCallback(uint percentageCompleted);
347
348private const int RM_SESSION_KEY_LEN = 16; // sizeof GUID
349private const int CCH_RM_SESSION_KEY = RM_SESSION_KEY_LEN * 2;
350private const int CCH_RM_MAX_APP_NAME = 255;
351private const int CCH_RM_MAX_SVC_NAME = 63;
352private const int RM_INVALID_TS_SESSION = -1;
353private const int RM_INVALID_PROCESS = -1;
354private const int ERROR_MORE_DATA = 234;
355
356[StructLayout(LayoutKind.Sequential)]
357public struct RmUniqueProcess
358{
359public int dwProcessId; // PID
360public FILETIME ProcessStartTime; // Process creation time
361}
362
363public enum RmAppType
364{
365/// <summary>
366/// Application type cannot be classified in known categories
367/// </summary>
368RmUnknownApp = 0,
369
370/// <summary>
371/// Application is a windows application that displays a top-level window
372/// </summary>
373RmMainWindow = 1,
374
375/// <summary>
376/// Application is a windows app but does not display a top-level window
377/// </summary>
378RmOtherWindow = 2,
379
380/// <summary>
381/// Application is an NT service
382/// </summary>
383RmService = 3,
384
385/// <summary>
386/// Application is Explorer
387/// </summary>
388RmExplorer = 4,
389
390/// <summary>
391/// Application is Console application
392/// </summary>
393RmConsole = 5,
394
395/// <summary>
396/// Application is critical system process where a reboot is required to restart
397/// </summary>
398RmCritical = 1000,
399}
400
401[Flags]
402public enum RmRebootReason
403{
404/// <summary>
405/// A system restart is not required.
406/// </summary>
407RmRebootReasonNone = 0x0,
408
409/// <summary>
410/// The current user does not have sufficient privileges to shut down one or more processes.
411/// </summary>
412RmRebootReasonPermissionDenied = 0x1,
413
414/// <summary>
415/// One or more processes are running in another Terminal Services session.
416/// </summary>
417RmRebootReasonSessionMismatch = 0x2,
418
419/// <summary>
420/// A system restart is needed because one or more processes to be shut down are critical processes.
421/// </summary>
422RmRebootReasonCriticalProcess = 0x4,
423
424/// <summary>
425/// A system restart is needed because one or more services to be shut down are critical services.
426/// </summary>
427RmRebootReasonCriticalService = 0x8,
428
429/// <summary>
430/// A system restart is needed because the current process must be shut down.
431/// </summary>
432RmRebootReasonDetectedSelf = 0x10,
433}
434
435[Flags]
436private enum RmShutdownType
437{
438RmForceShutdown = 0x1, // Force app shutdown
439RmShutdownOnlyRegistered = 0x10 // Only shutdown apps if all apps registered for restart
440}
441
442[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
443public struct RmProcessInfo
444{
445public RmUniqueProcess UniqueProcess;
446
447[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)]
448public string AppName;
449
450[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)]
451public string ServiceShortName;
452
453public RmAppType ApplicationType;
454public int AppStatus;
455public int TSSessionId;
456
457[MarshalAs(UnmanagedType.Bool)]
458public bool bRestartable;
459
460public Process Process
461{
462get
463{
464try
465{
466Process process = Process.GetProcessById(UniqueProcess.dwProcessId);
467long fileTime = process.StartTime.ToFileTime();
468
469if ((uint)UniqueProcess.ProcessStartTime.dwLowDateTime != (uint)(fileTime & uint.MaxValue))
470return null;
471
472if ((uint)UniqueProcess.ProcessStartTime.dwHighDateTime != (uint)(fileTime >> 32))
473return null;
474
475return process;
476}
477catch (Exception)
478{
479return null;
480}
481}
482}
483}
484
485[DllImport("rstrtmgr", CharSet = CharSet.Unicode)]
486private static extern int RmStartSession(out int dwSessionHandle, int sessionFlags, StringBuilder strSessionKey);
487
488[DllImport("rstrtmgr")]
489private static extern int RmEndSession(int dwSessionHandle);
490
491[DllImport("rstrtmgr")]
492private static extern int RmShutdown(int dwSessionHandle, RmShutdownType lAtionFlags, RmWriteStatusCallback fnStatus);
493
494[DllImport("rstrtmgr")]
495private static extern int RmRestart(int dwSessionHandle, int dwRestartFlags, RmWriteStatusCallback fnStatus);
496
497[DllImport("rstrtmgr")]
498private static extern int RmGetList(int dwSessionHandle, out int nProcInfoNeeded, ref int nProcInfo, [In, Out] RmProcessInfo[] rgAffectedApps, out RmRebootReason dwRebootReasons);
499
500[DllImport("rstrtmgr", CharSet = CharSet.Unicode)]
501private static extern int RmRegisterResources(int dwSessionHandle,
502int nFiles, string[] rgsFileNames,
503int nApplications, RmUniqueProcess[] rgApplications,
504int nServices, string[] rgsServiceNames);
505
506private readonly int sessionHandle;
507private readonly string sessionKey;
508
509public WindowsRestartManager()
510{
511var sessKey = new StringBuilder(CCH_RM_SESSION_KEY + 1);
512ThrowOnFailure(RmStartSession(out sessionHandle, 0, sessKey));
513sessionKey = sessKey.ToString();
514}
515
516public void Register(IEnumerable<FileInfo> files = null, IEnumerable<Process> processes = null, IEnumerable<string> serviceNames = null)
517{
518string[] filesArray = files?.Select(f => f.FullName).ToArray() ?? Array.Empty<string>();
519RmUniqueProcess[] processesArray = processes?.Select(f => new RmUniqueProcess
520{
521dwProcessId = f.Id,
522ProcessStartTime = new FILETIME
523{
524dwLowDateTime = (int)(f.StartTime.ToFileTime() & uint.MaxValue),
525dwHighDateTime = (int)(f.StartTime.ToFileTime() >> 32),
526}
527}).ToArray() ?? Array.Empty<RmUniqueProcess>();
528string[] servicesArray = serviceNames?.ToArray() ?? Array.Empty<string>();
529ThrowOnFailure(RmRegisterResources(sessionHandle,
530filesArray.Length, filesArray,
531processesArray.Length, processesArray,
532servicesArray.Length, servicesArray));
533}
534
535public void Shutdown(bool forceShutdown = true, bool shutdownOnlyRegistered = false, RmWriteStatusCallback cb = null)
536{
537ThrowOnFailure(RmShutdown(sessionHandle, (forceShutdown ? RmShutdownType.RmForceShutdown : 0) | (shutdownOnlyRegistered ? RmShutdownType.RmShutdownOnlyRegistered : 0), cb));
538}
539
540public void Restart(RmWriteStatusCallback cb = null)
541{
542ThrowOnFailure(RmRestart(sessionHandle, 0, cb));
543}
544
545public List<RmProcessInfo> GetInterferingProcesses(out RmRebootReason rebootReason)
546{
547var count = 0;
548var infos = new RmProcessInfo[count];
549var err = 0;
550
551for (var i = 0; i < 16; i++)
552{
553err = RmGetList(sessionHandle, out int needed, ref count, infos, out rebootReason);
554
555switch (err)
556{
557case 0:
558return infos.Take(count).ToList();
559
560case ERROR_MORE_DATA:
561infos = new RmProcessInfo[count = needed];
562break;
563
564default:
565ThrowOnFailure(err);
566break;
567}
568}
569
570ThrowOnFailure(err);
571
572// should not reach
573throw new InvalidOperationException();
574}
575
576private void ReleaseUnmanagedResources()
577{
578ThrowOnFailure(RmEndSession(sessionHandle));
579}
580
581public void Dispose()
582{
583ReleaseUnmanagedResources();
584GC.SuppressFinalize(this);
585}
586
587~WindowsRestartManager()
588{
589ReleaseUnmanagedResources();
590}
591
592private void ThrowOnFailure(int err)
593{
594if (err != 0)
595throw new Win32Exception(err);
596}
597}
598public class WindowsSteam : ISteam
599{
600public WindowsSteam()
601{
602SteamUtils.OnGamepadTextInputDismissed += b => OnGamepadTextInputDismissed?.Invoke(b);
603}
604
605public void Initialize(uint appId)
606{
607// workaround because SetEnvironmentVariable doesn't actually touch the process environment on unix
608if (Environment.OSVersion.Platform == PlatformID.Unix)
609{
610[System.Runtime.InteropServices.DllImport("c")]
611static extern int setenv(string name, string value, int overwrite);
612
613setenv("SteamAppId", appId.ToString(), 1);
614}
615
616SteamClient.Init(appId);
617}
618
619public bool IsValid => SteamClient.IsValid;
620
621public bool BLoggedOn => SteamClient.IsLoggedOn;
622
623public bool BOverlayNeedsPresent => SteamUtils.DoesOverlayNeedPresent;
624
625public void Shutdown()
626{
627SteamClient.Shutdown();
628}
629
630public async Task<byte[]?> GetAuthSessionTicketAsync()
631{
632var ticket = await SteamUser.GetAuthSessionTicketAsync().ConfigureAwait(true);
633return ticket?.Data;
634}
635
636public bool IsAppInstalled(uint appId)
637{
638return SteamApps.IsAppInstalled(appId);
639}
640
641public string GetAppInstallDir(uint appId)
642{
643return SteamApps.AppInstallDir(appId);
644}
645
646public bool ShowGamepadTextInput(bool password, bool multiline, string description, int maxChars, string existingText = "")
647{
648return SteamUtils.ShowGamepadTextInput(password ? GamepadTextInputMode.Password : GamepadTextInputMode.Normal, multiline ? GamepadTextInputLineMode.MultipleLines : GamepadTextInputLineMode.SingleLine, description, maxChars, existingText);
649}
650
651public string GetEnteredGamepadText()
652{
653return SteamUtils.GetEnteredGamepadText();
654}
655
656public bool ShowFloatingGamepadTextInput(ISteam.EFloatingGamepadTextInputMode mode, int x, int y, int width, int height)
657{
658// Facepunch.Steamworks doesn't have this...
659return false;
660}
661
662public bool IsRunningOnSteamDeck() => false;
663
664public uint GetServerRealTime() => (uint)((DateTimeOffset)SteamUtils.SteamServerTime).ToUnixTimeSeconds();
665
666public void ActivateGameOverlayToWebPage(string url, bool modal = false)
667{
668SteamFriends.OpenWebOverlay(url, modal);
669}
670
671public event Action<bool> OnGamepadTextInputDismissed;
672}
673}
674