From 8ee7ed23e4552d1eb768bdb4575010ccb143e039 Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Wed, 31 Jan 2024 23:02:14 +0100 Subject: [PATCH] Release 0.9.9.12 (#558) * Release 0.9.9.12 * Move injection to injector executable (#554) * Fix logview missing history (#556) Closes #526 * Detect unresponsive Guild Wars instance (#557) Closes #534 Closes #422 --- .github/workflows/cd.yaml | 7 +- .github/workflows/ci.yaml | 2 +- Daybreak.Injector/Daybreak.Injector.csproj | 19 + Daybreak.Injector/NativeMethods.cs | 469 ++++++++++++++++++ Daybreak.Injector/ProcessInjector.cs | 141 ++++++ Daybreak.Injector/Program.cs | 63 +++ Daybreak.sln | 22 +- .../Templates/LaunchButtonTemplate.xaml | 25 +- .../Templates/LaunchButtonTemplate.xaml.cs | 31 +- Daybreak/Daybreak.csproj | 2 +- Daybreak/Models/LauncherViewContext.cs | 12 + .../ApplicationLauncher.cs | 34 +- .../IApplicationLauncher.cs | 2 +- Daybreak/Services/GWCA/GWCAClient.cs | 2 +- .../Services/Injection/ProcessInjector.cs | 153 ++---- Daybreak/Services/Scanner/GWCAMemoryReader.cs | 182 +++---- Daybreak/Views/FocusView.xaml.cs | 128 +++-- Daybreak/Views/LauncherView.xaml.cs | 133 +++-- Daybreak/Views/LogsView.xaml.cs | 11 +- 19 files changed, 1111 insertions(+), 327 deletions(-) create mode 100644 Daybreak.Injector/Daybreak.Injector.csproj create mode 100644 Daybreak.Injector/NativeMethods.cs create mode 100644 Daybreak.Injector/ProcessInjector.cs create mode 100644 Daybreak.Injector/Program.cs diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 8af89fc3..0a1bca33 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - targetplatform: [x86] + targetplatform: [x64] runs-on: windows-latest @@ -97,6 +97,11 @@ jobs: env: RuntimeIdentifier: win-${{ matrix.targetplatform }} + - name: Create publish injector files + run: dotnet publish .\Daybreak.Injector\Daybreak.Injector.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=true --self-contained true -o .\Publish + env: + RuntimeIdentifier: win-x86 + - name: Pack publish files run: | Write-Host $env diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a4f0493f..2d174def 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: strategy: matrix: - targetplatform: [x86] + targetplatform: [x64] runs-on: windows-latest diff --git a/Daybreak.Injector/Daybreak.Injector.csproj b/Daybreak.Injector/Daybreak.Injector.csproj new file mode 100644 index 00000000..28794419 --- /dev/null +++ b/Daybreak.Injector/Daybreak.Injector.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + Daybreak.Injector + enable + enable + AnyCPU;x86 + True + + + + + + + + + diff --git a/Daybreak.Injector/NativeMethods.cs b/Daybreak.Injector/NativeMethods.cs new file mode 100644 index 00000000..74531eb3 --- /dev/null +++ b/Daybreak.Injector/NativeMethods.cs @@ -0,0 +1,469 @@ +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; + +namespace Daybreak.Injector; + +internal static class NativeMethods +{ + public static uint WM_KEYDOWN = 0x0100; + public static uint SWP_SHOWWINDOW = 0x0040; + public static IntPtr HWND_TOPMOST = new(-1); + public static IntPtr HWND_TOP = IntPtr.Zero; + public const int WH_KEYBOARD_LL = 13; + public const uint LIST_MODULES_32BIT = 0x01; + + public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + internal struct ProcessEntry32 + { + public uint dwSize; + public uint cntUsage; + public uint th32ProcessID; + public IntPtr th32DefaultHeapID; + public uint th32ModuleID; + public uint cntThreads; + public uint th32ParentProcessID; + public int pcPriClassBase; + public uint dwFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string szExeFile; + }; + [StructLayout(LayoutKind.Sequential)] + internal struct PEB + { + private readonly byte InheritedAddressSpace; + private readonly byte ReadImageFileExecOptions; + private readonly byte BeingDebugged; + private readonly byte BitField; + private readonly IntPtr Mutant; + internal IntPtr ImageBaseAddress; + } + [StructLayout(LayoutKind.Sequential)] + public struct SecurityAttributes + { + public uint nLength; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; + } + [Flags] + public enum MemoryProtection : uint + { + PAGE_EXECUTE = 0x10, + PAGE_EXECUTE_READ = 0x20, + PAGE_EXECUTE_READ_WRITE = 0x40, + PAGE_EXECUTE_WRITECOPY = 0x80, + PAGE_NOACCESS = 0x01, + PAGE_READONLY = 0x02, + PAGE_READWRITE = 0x04, + PAGE_WRITECOPY = 0x08, + PAGE_GUARD = 0x100, + PAGE_NOCACHE = 0x200, + PAGE_WRITECOMBINE = 0x400 + } + [Flags] + public enum MemoryAllocationType : uint + { + MEM_COMMIT = 0x00001000, + MEM_RESERVE = 0x00002000, + MEM_RESET = 0x00080000, + MEM_RESET_UNDO = 0x1000000, + MEM_LARGE_PAGES = 0x20000000, + MEM_PHYSICAL = 0x00400000, + MEM_TOP_DOWN = 0x00100000, + MEM_WRITE_WATCH = 0x00200000 + } + [StructLayout(LayoutKind.Sequential)] + public readonly struct RECT + { + public readonly int Left, Top, Right, Bottom; + + public int Height => this.Bottom - this.Top; + + public int Width => this.Right - this.Left; + + public RECT(int left, int top, int right, int bottom) + { + this.Left = left; + this.Top = top; + this.Right = right; + this.Bottom = bottom; + } + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SystemHandleInformation + { + public uint OwnerPID; + public byte ObjectType; + public byte HandleFlags; + public ushort HandleValue; + public UIntPtr ObjectPointer; + public IntPtr AccessMask; + } + [StructLayout(LayoutKind.Sequential)] + public struct ObjectBasicInformation + { + public uint Attributes; + public uint GrantedAccess; + public uint HandleCount; + public uint PointerCount; + public uint PagedPoolUsage; + public uint NonPagedPoolUsage; + public uint Reserved1; + public uint Reserved2; + public uint Reserved3; + public uint NameInformationLength; + public uint TypeInformationLength; + public uint SecurityDescriptorLength; + public FILETIME CreateTime; + } + [StructLayout(LayoutKind.Sequential)] + public struct IoStatusBlock + { + public uint Status; + public ulong Information; + } + [StructLayout(LayoutKind.Sequential)] + public struct WindowInfo + { + public uint cbSize; + public RECT rcWindow; + public RECT rcClient; + public uint dwStyle; + public uint dwExStyle; + public uint dwWindowStatus; + public uint cxWindowBorders; + public uint cyWindowBorders; + public ushort atomWindowType; + public ushort wCreatorVersion; + + public WindowInfo(bool? _) : this() // Allows automatic initialization of "cbSize" with "new WINDOWINFO(null/true/false)". + { + this.cbSize = (uint)(Marshal.SizeOf(typeof(WindowInfo))); + } + + } + [Flags] + public enum DuplicateOptions : uint + { + DUPLICATE_CLOSE_SOURCE = 0x00000001, + DUPLICATE_SAME_ACCESS = 0x00000002 + } + [Flags] + public enum ProcessAccessFlags : uint + { + All = 0x001F0FFF, + Terminate = 0x00000001, + CreateThread = 0x00000002, + VMOperation = 0x00000008, + VMRead = 0x00000010, + VMWrite = 0x00000020, + DupHandle = 0x00000040, + SetInformation = 0x00000200, + QueryInformation = 0x00000400, + QueryLimitedInformation = 0x00001000, + Synchronize = 0x00100000 + } + [Flags] + public enum NtStatus : uint + { + STATUS_SUCCESS = 0x00000000, + STATUS_INFO_LENGTH_MISMATCH = 0xC0000004 + } + [Flags] + public enum ObjectInformationClass : uint + { + ObjectBasicInformation = 0, + ObjectNameInformation = 1, + ObjectTypeInformation = 2, + ObjectAllTypesInformation = 3, + ObjectHandleInformation = 4 + } + [Flags] + public enum SystemInformationClass : uint + { + SystemHandleInformation = 16 + } + [Flags] + public enum FileInformationClass + { + FileNameInformation = 9 + } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct StartupInfo + { + public int cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public int dwX; + public int dwY; + public int dwXSize; + public int dwYSize; + public int dwXCountChars; + public int dwYCountChars; + public int dwFillAttribute; + public int dwFlags; + public short wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + [StructLayout(LayoutKind.Sequential)] + public struct ProcessInformation + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + public enum ProcessInfoClass : uint + { + ProcessBasicInformation = 0x00, + ProcessDebugPort = 0x07, + ProcessExceptionPort = 0x08, + ProcessAccessToken = 0x09, + ProcessWow64Information = 0x1A, + ProcessImageFileName = 0x1B, + ProcessDebugObjectHandle = 0x1E, + ProcessDebugFlags = 0x1F, + ProcessExecuteFlags = 0x22, + ProcessInstrumentationCallback = 0x28, + MaxProcessInfoClass = 0x64 + } + [StructLayout(LayoutKind.Sequential)] + internal struct ProcessBasicInformation + { + private readonly IntPtr Reserved1; + internal IntPtr PebBaseAddress; + private readonly IntPtr Reserved2; + private readonly IntPtr Reserved3; + private readonly UIntPtr UniqueProcessId; + private readonly IntPtr Reserved4; + } + public enum SaferLevel : uint + { + Disallowed = 0, + Untrusted = 0x1000, + Constrained = 0x10000, + NormalUser = 0x20000, + FullyTrusted = 0x40000 + } + public enum SaferLevelScope : uint + { + Machine = 1, + User = 2 + } + public enum SaferOpen : uint + { + Open = 1 + } + public enum SaferTokenBehaviour : uint + { + Default = 0x0, + NullIfEqual = 0x1, + CompareOnly = 0x2, + MakeInert = 0x4, + WantFlags = 0x8 + } + public enum TokenInformationClass : uint + { + TokenUser = 1, + TokenGroups, + TokenPrivileges, + TokenOwner, + TokenPrimaryGroup, + TokenDefaultDacl, + TokenSource, + TokenType, + TokenImpersonationLevel, + TokenStatistics, + TokenRestrictedSids, + TokenSessionId, + TokenGroupsAndPrivileges, + TokenSessionReference, + TokenSandBoxInert, + TokenAuditPolicy, + TokenOrigin, + TokenElevationType, + TokenLinkedToken, + TokenElevation, + TokenHasRestrictions, + TokenAccessInformation, + TokenVirtualizationAllowed, + TokenVirtualizationEnabled, + TokenIntegrityLevel, + TokenUiAccess, + TokenMandatoryPolicy, + TokenLogonSid, + MaxTokenInfoClass + } + [StructLayout(LayoutKind.Sequential)] + public struct TokenMandatoryLabel + { + public SidAndAttributes Label; + } + [StructLayout(LayoutKind.Sequential)] + public struct SidAndAttributes + { + public IntPtr Sid; + public int Attributes; + } + [StructLayout(LayoutKind.Sequential)] + public struct Sid_Identifier_Authority + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public byte[] Value; + } + [Flags] + public enum CreationFlags : uint + { + CreateSuspended = 0x00000004, + DetachedProcess = 0x00000008, + CreateNoWindow = 0x08000000, + ExtendedStartupInfoPresent = 0x00080000 + } + + public const int WM_SYSCOMMAND = 0x112; + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID); + [DllImport("kernel32.dll")] + public static extern bool Process32First(IntPtr hSnapshot, ref ProcessEntry32 lppe); + [DllImport("kernel32.dll")] + public static extern bool Process32Next(IntPtr hSnapshot, ref ProcessEntry32 lppe); + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + [DllImport("kernel32.dll")] + public static extern bool CloseHandle(IntPtr hObject); + [DllImport("kernel32.dll")] + public static extern bool DuplicateHandle(IntPtr hSourceProcessHandle, IntPtr hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, DuplicateOptions dwOptions); + [DllImport("kernel32.dll")] + public static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, bool bInheritHandle, uint dwProcessID); + [DllImport("ntdll.dll", SetLastError = true)] + public static extern NtStatus NtQueryInformationFile(IntPtr FileHandle, ref IoStatusBlock IoStatusBlock, IntPtr FileInformation, int FileInformationLength, FileInformationClass FileInformationClass); + [DllImport("ntdll.dll")] + public static extern NtStatus NtQueryObject(IntPtr ObjectHandle, ObjectInformationClass ObjectInformationClass, IntPtr ObjectInformation, int ObjectInformationLength, out int ReturnLength); + [DllImport("ntdll.dll")] + public static extern NtStatus NtQuerySystemInformation(SystemInformationClass SystemInformationClass, IntPtr SystemInformation, int SystemInformationLength, out int ReturnLength); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + public static extern bool QueryFullProcessImageName(IntPtr hProcess, uint dwFlags, StringBuilder lpExeName, ref uint lpdwSize); + [DllImport("user32.dll")] + public static extern bool SetWindowPos(IntPtr hwnd, IntPtr insertAfter, int x, int y, int cx, int cy, uint flags); + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hwnd, int cmd); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern int GetWindowTextLength(IntPtr hWnd); + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr LoadLibrary(string lpFileName); + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + public static extern bool FreeLibrary(IntPtr hModule); + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool UnhookWindowsHookEx(IntPtr hHook); + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr CallNextHookEx(IntPtr hHook, int code, IntPtr wParam, IntPtr lParam); + [DllImport("user32.dll")] + public static extern IntPtr GetForegroundWindow(); + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool ReadProcessMemory(IntPtr hProcess, uint lpBaseAddress, IntPtr lpBuffer, uint nSize, out uint lpNumberOfBytesRead); + [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi)] + internal static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead); + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetWindowInfo(IntPtr hwnd, ref WindowInfo pwi); + [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi)] + public static extern bool CreateProcess( + string lpApplicationName, string lpCommandLine, ref SecurityAttributes lpProcessAttributes, + ref SecurityAttributes lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, + IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref StartupInfo lpStartupInfo, + out ProcessInformation lpProcessInformation); + [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi)] + public static extern uint ResumeThread(IntPtr hThread); + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr LocalFree(IntPtr hMem); + [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi)] + public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesWritten); + [DllImport("ntdll.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)] + public static extern int NtQueryInformationProcess(IntPtr hProcess, ProcessInfoClass pic, out ProcessBasicInformation pbi, int cb, out int pSize); + [DllImport("advapi32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)] + public static extern bool SaferCreateLevel(SaferLevelScope scopeId, SaferLevel levelId, SaferOpen openFlags, out IntPtr levelHandle, IntPtr reserved); + [DllImport("advapi32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)] + public static extern bool SaferComputeTokenFromLevel(IntPtr levelHandle, IntPtr inAccessToken, out IntPtr outAccessToken, SaferTokenBehaviour flags, IntPtr lpReserved); + [DllImport("advapi32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)] + public static extern bool SaferCloseLevel(IntPtr levelHandle); + [DllImport("advapi32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)] + public static extern bool SetTokenInformation(IntPtr tokenHandle, TokenInformationClass tokenInformationokenInformationClass, ref TokenMandatoryLabel tokenInformation, uint tokenInformationLength); + [DllImport("advapi32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)] + public static extern bool ConvertStringSidToSid(string stringSid, out IntPtr ptrSid); + [DllImport("advapi32.dll")] + public static extern uint GetLengthSid(IntPtr pSid); + [DllImport("advapi32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)] + public static extern bool CreateProcessAsUser( + IntPtr hToken, + string lpApplicationName, + string lpCommandLine, + ref SecurityAttributes lpProcessAttributes, + ref SecurityAttributes lpThreadAttributes, + bool bInheritHandles, + uint dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + ref StartupInfo lpStartupInfo, + out ProcessInformation lpProcessInformation); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr GetModuleHandle(string lpModuleName); + [DllImport("kernel32.dll")] + public static extern IntPtr CreateRemoteThread( + IntPtr hProcess, + IntPtr lpThreadAttributes, + uint dwStackSize, + IntPtr lpStartAddress, + IntPtr lpParameter, + uint dwCreationFlags, + out IntPtr lpThreadId); + + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + public static extern IntPtr VirtualAllocEx( + IntPtr hProcess, + IntPtr lpAddress, + IntPtr dwSize, + uint dwAllocationType, + uint dwProtect); + + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + public static extern bool VirtualFreeEx( + IntPtr hProcess, + IntPtr lpAddress, + uint dwSize, + uint dwFreeType); + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] + public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint GetExitCodeThread(IntPtr hHandle, out IntPtr dwMilliseconds); + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool WriteProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + IntPtr lpBuffer, + int nSize, + out IntPtr lpNumberOfBytesWritten); + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool ReadProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + IntPtr lpBuffer, + int nSize, + out IntPtr lpNumberOfBytesRead); +} diff --git a/Daybreak.Injector/ProcessInjector.cs b/Daybreak.Injector/ProcessInjector.cs new file mode 100644 index 00000000..6b716413 --- /dev/null +++ b/Daybreak.Injector/ProcessInjector.cs @@ -0,0 +1,141 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Daybreak.Injector; +internal static class ProcessInjector +{ + public static Task Inject(Process process, string pathToDll, CancellationToken cancellationToken) + { + return Task.Factory.StartNew(() => + { + return InjectWithWinApi(process, pathToDll); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + } + + private static bool InjectWithWinApi(Process process, string pathToDll) + { + var modulefullpath = Path.GetFullPath(pathToDll); + + if (!File.Exists(modulefullpath)) + { + Console.WriteLine("Dll to inject not found"); + return false; + } + + var hKernel32 = NativeMethods.GetModuleHandle("kernel32.dll"); + if (hKernel32 == IntPtr.Zero) + { + Console.WriteLine("Unable to get a handle of kernel32.dll"); + return false; + } + + var hLoadLib = NativeMethods.GetProcAddress(hKernel32, "LoadLibraryW"); + if (hLoadLib == IntPtr.Zero) + { + Console.WriteLine("Unable to get the address of LoadLibraryW"); + return false; + } + + var hStringBuffer = NativeMethods.VirtualAllocEx(process.Handle, IntPtr.Zero, new IntPtr(2 * (modulefullpath.Length + 1)), + 0x3000 /* MEM_COMMIT | MEM_RESERVE */, 0x4 /* PAGE_READWRITE */); + if (hStringBuffer == IntPtr.Zero) + { + Console.WriteLine("Unable to allocate memory for module path"); + return false; + } + + WriteWString(process, hStringBuffer, modulefullpath); + if (ReadWString(process, hStringBuffer, 260) != modulefullpath) + { + Console.WriteLine("Module path string is not correct"); + return false; + } + + var hThread = NativeMethods.CreateRemoteThread(process.Handle, IntPtr.Zero, 0, hLoadLib, hStringBuffer, 0, out _); + if (hThread == IntPtr.Zero) + { + Console.WriteLine("Unable to create remote thread"); + return false; + } + + var threadResult = NativeMethods.WaitForSingleObject(hThread, 30000u); + if (threadResult is 0x102 or 0xFFFFFFFF /* WAIT_FAILED */) + { + Console.WriteLine($"Exception occurred while waiting for the remote thread. Result is {threadResult}"); + return false; + } + + var dllResult = NativeMethods.GetExitCodeThread(hThread, out _); + if (dllResult == 0) + { + Console.WriteLine($"Injected dll returned non-success status code {dllResult}"); + return false; + } + + var memoryFreeResult = NativeMethods.VirtualFreeEx(process.Handle, hStringBuffer, 0, 0x8000 /* MEM_RELEASE */); + if (!memoryFreeResult) + { + Console.WriteLine($"Failed to free dll memory"); + } + + return memoryFreeResult; + } + + private static void WriteBytes(Process process, IntPtr address, byte[] data) + { + var size = data.Length; + var buffer = Marshal.AllocHGlobal(size); + Marshal.Copy(data, 0, buffer, size); + + NativeMethods.WriteProcessMemory( + process.Handle, + address, + buffer, + size, + out _); + + Marshal.FreeHGlobal(buffer); + } + + private static void WriteWString(Process process, IntPtr address, string data) + { + WriteBytes(process, address, Encoding.Unicode.GetBytes(data)); + } + + private static string ReadWString(Process process, IntPtr address, int maxsize, Encoding? encoding = null) + { + encoding ??= Encoding.Unicode; + var rawbytes = ReadBytes(process, address, maxsize); + if (rawbytes.Length == 0) + { + return ""; + } + + var ret = encoding.GetString(rawbytes); + if (ret.Contains('\0')) + { + ret = ret[..ret.IndexOf('\0')]; + } + + return ret; + } + + private static byte[] ReadBytes(Process process, IntPtr address, int size) + { + var buffer = Marshal.AllocHGlobal(size); + + NativeMethods.ReadProcessMemory(process.Handle, + address, + buffer, + size, + out _ + ); + + var ret = new byte[size]; + Marshal.Copy(buffer, ret, 0, size); + Marshal.FreeHGlobal(buffer); + + return ret; + } +} diff --git a/Daybreak.Injector/Program.cs b/Daybreak.Injector/Program.cs new file mode 100644 index 00000000..2ab72a1a --- /dev/null +++ b/Daybreak.Injector/Program.cs @@ -0,0 +1,63 @@ +using Daybreak.Injector; +using System.Diagnostics; + +var processId = -1; +var pathToDll = string.Empty; + +if (args.Length != 4) +{ + Console.WriteLine("Daybreak Injector"); + Console.WriteLine("Usage:"); + Console.WriteLine("Daybreak.Injector.exe -p [PROCESS_ID] -d [PATH_TO_DLL]"); + return -1; +} + +for (var i = 0; i < args.Length - 1; i++) +{ + if (args[i].ToLower() == "-p" && + int.TryParse(args[i + 1], out var parsedId)) + { + processId = parsedId; + i++; + } + else if (args[i].ToLower() == "-d") + { + pathToDll = args[i + 1]; + i++; + } +} + +if (processId == -1) +{ + Console.WriteLine("Error: Process id was not specified"); + return -1; +} + +if (pathToDll == string.Empty) +{ + Console.WriteLine("Error: Path to dll was not specified"); + return -1; +} + +if (!File.Exists(pathToDll)) +{ + Console.WriteLine("Error: Provided dll could not be found"); + return -1; +} + +var maybeProcess = Process.GetProcessById(processId); +if (maybeProcess is null) +{ + Console.WriteLine("Error: Could not find desired process"); + return -1; +} + +var result = await ProcessInjector.Inject(maybeProcess, pathToDll, CancellationToken.None); +if (result is false) +{ + Console.WriteLine("Error: Failed to inject dll"); + return -1; +} + +Console.WriteLine("Injected dll"); +return 0; diff --git a/Daybreak.sln b/Daybreak.sln index b07aaebc..23d2fb88 100644 --- a/Daybreak.sln +++ b/Daybreak.sln @@ -34,6 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Daybreak.7ZipExtractor", "Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj", "{CA60F9AC-A496-4DB6-970E-ECE73B051C05}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Daybreak.Injector", "Daybreak.Injector\Daybreak.Injector.csproj", "{2C1AB589-A533-41B6-9F8B-4328AD122437}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -70,8 +72,8 @@ Global {C911C1C2-6566-419C-89C7-F802EB3FE709}.Release|x86.Build.0 = Release|x86 {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x64.ActiveCfg = Debug|x64 - {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x64.Build.0 = Debug|x64 + {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x64.ActiveCfg = Debug|x86 + {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x64.Build.0 = Debug|x86 {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x86.ActiveCfg = Debug|x86 {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x86.Build.0 = Debug|x86 {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -82,8 +84,8 @@ Global {4E2BB805-135D-4F02-8C53-3D8B6876D323}.Release|x86.Build.0 = Release|x86 {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Debug|x64.ActiveCfg = Debug|Any CPU - {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Debug|x64.Build.0 = Debug|Any CPU + {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Debug|x64.ActiveCfg = Debug|x64 + {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Debug|x64.Build.0 = Debug|x64 {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Debug|x86.ActiveCfg = Debug|x64 {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Debug|x86.Build.0 = Debug|x64 {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -92,6 +94,18 @@ Global {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Release|x64.Build.0 = Release|Any CPU {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Release|x86.ActiveCfg = Release|x64 {CA60F9AC-A496-4DB6-970E-ECE73B051C05}.Release|x86.Build.0 = Release|x64 + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Debug|x64.ActiveCfg = Debug|x86 + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Debug|x64.Build.0 = Debug|x86 + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Debug|x86.ActiveCfg = Debug|x86 + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Debug|x86.Build.0 = Debug|x86 + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Release|Any CPU.Build.0 = Release|Any CPU + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Release|x64.ActiveCfg = Release|Any CPU + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Release|x64.Build.0 = Release|Any CPU + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Release|x86.ActiveCfg = Release|Any CPU + {2C1AB589-A533-41B6-9F8B-4328AD122437}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml b/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml index 121b3a27..600c3fe6 100644 --- a/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml +++ b/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml @@ -19,7 +19,7 @@ + Visibility="{Binding ElementName=_this, Path=CanLaunch, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"> - + Visibility="{Binding ElementName=_this, Path=CanAttach, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"> + + + + + + + diff --git a/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml.cs b/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml.cs index 9cf930a8..e2e418c3 100644 --- a/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml.cs +++ b/Daybreak/Controls/Templates/LaunchButtonTemplate.xaml.cs @@ -31,8 +31,14 @@ public partial class LaunchButtonTemplate : UserControl private readonly IApplicationLauncher applicationLauncher; private readonly ILiveOptions liveOptions; + [GenerateDependencyProperty(InitialValue = true)] + private bool canLaunch; + [GenerateDependencyProperty] - private bool canShowFocusView; + private bool canKill; + + [GenerateDependencyProperty] + private bool canAttach; [GenerateDependencyProperty] private bool gameRunning; @@ -101,14 +107,20 @@ public partial class LaunchButtonTemplate : UserControl if (this.DataContext is not LauncherViewContext launcherViewContext || launcherViewContext.Configuration is null) { + this.CanLaunch = false; + this.CanAttach = false; + this.CanKill = false; return; } if (this.applicationLauncher.GetGuildwarsProcess(launcherViewContext.Configuration) is not GuildWarsApplicationLaunchContext context) { this.GameRunning = false; - this.CanShowFocusView = this.liveOptions.Value.Enabled && this.GameRunning; + this.CanLaunch = true; + this.CanAttach = this.liveOptions.Value.Enabled && this.GameRunning; + this.CanKill = false; launcherViewContext.CanLaunch = true; + launcherViewContext.CanKill = false; return; } @@ -119,8 +131,11 @@ public partial class LaunchButtonTemplate : UserControl catch { this.GameRunning = false; - this.CanShowFocusView = this.liveOptions.Value.Enabled && this.GameRunning; + this.CanLaunch = false; + this.CanAttach = this.liveOptions.Value.Enabled && this.GameRunning; + this.CanKill = this.CanAttach ? false : true; launcherViewContext.CanLaunch = false; + launcherViewContext.CanKill = true; return; } @@ -128,13 +143,19 @@ public partial class LaunchButtonTemplate : UserControl if (loginInfo?.Email != context.LaunchConfiguration.Credentials?.Username) { this.GameRunning = false; - this.CanShowFocusView = this.liveOptions.Value.Enabled && this.GameRunning; + this.CanAttach = this.liveOptions.Value.Enabled && this.GameRunning; + this.CanLaunch = true; + this.CanKill = false; launcherViewContext.CanLaunch = false; + launcherViewContext.CanKill = false; return; } this.GameRunning = true; + this.CanKill = false; + this.CanLaunch = false; launcherViewContext.CanLaunch = true; - this.CanShowFocusView = this.liveOptions.Value.Enabled && this.GameRunning; + launcherViewContext.CanKill = false; + this.CanAttach = this.liveOptions.Value.Enabled && this.GameRunning; } } diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index 09ee7fbd..719d8a9b 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -11,7 +11,7 @@ preview Daybreak.ico true - 0.9.9.11 + 0.9.9.12 true cfb2a489-db80-448d-a969-80270f314c46 True diff --git a/Daybreak/Models/LauncherViewContext.cs b/Daybreak/Models/LauncherViewContext.cs index b8634917..0322716f 100644 --- a/Daybreak/Models/LauncherViewContext.cs +++ b/Daybreak/Models/LauncherViewContext.cs @@ -5,10 +5,12 @@ namespace Daybreak.Models; public sealed class LauncherViewContext : INotifyPropertyChanged { private bool canLaunch = false; + private bool canKill = false; public event PropertyChangedEventHandler? PropertyChanged; public LaunchConfigurationWithCredentials? Configuration { get; init; } + public bool CanLaunch { get => this.canLaunch; @@ -18,4 +20,14 @@ public sealed class LauncherViewContext : INotifyPropertyChanged this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.CanLaunch))); } } + + public bool CanKill + { + get => this.canKill; + set + { + this.canKill = value; + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.CanKill))); + } + } } diff --git a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs index fbd8aacc..33632514 100644 --- a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs +++ b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs @@ -83,10 +83,7 @@ internal sealed class ApplicationLauncher : IApplicationLauncher return null; } - var gwProcess = await this.LaunchGuildwarsProcess( - credentials.Username!.ThrowIfNull(), - credentials.Password!.ThrowIfNull(), - launchConfigurationWithCredentials.ExecutablePath!.ThrowIfNull()); + var gwProcess = await this.LaunchGuildwarsProcess(launchConfigurationWithCredentials); if (gwProcess is null) { return default; @@ -163,8 +160,11 @@ internal sealed class ApplicationLauncher : IApplicationLauncher Application.Current.Shutdown(); } - private async Task LaunchGuildwarsProcess(string email, Models.SecureString password, string executable) + private async Task LaunchGuildwarsProcess(LaunchConfigurationWithCredentials launchConfigurationWithCredentials) { + var email = launchConfigurationWithCredentials.Credentials!.Username; + var password = launchConfigurationWithCredentials.Credentials!.Password; + var executable = launchConfigurationWithCredentials.ExecutablePath!; if (File.Exists(executable) is false) { throw new ExecutableNotFoundException($"Guildwars executable doesn't exist at {executable}"); @@ -293,7 +293,7 @@ internal sealed class ApplicationLauncher : IApplicationLauncher } catch (Exception e) { - this.KillGuildWarsProcess(process); + this.KillGuildWarsProcess(new GuildWarsApplicationLaunchContext { GuildWarsProcess = process, LaunchConfiguration = launchConfigurationWithCredentials, ProcessId = (uint)pId }); this.logger.LogError(e, $"{mod.Name} unhandled exception"); this.notificationService.NotifyError( title: $"{mod.Name} exception", @@ -364,7 +364,7 @@ internal sealed class ApplicationLauncher : IApplicationLauncher } catch (Exception e) { - this.KillGuildWarsProcess(process); + this.KillGuildWarsProcess(new GuildWarsApplicationLaunchContext { GuildWarsProcess = process, LaunchConfiguration = launchConfigurationWithCredentials, ProcessId = (uint)pId }); this.logger.LogError(e, $"{mod.Name} unhandled exception"); this.notificationService.NotifyError( title: $"{mod.Name} exception", @@ -399,18 +399,11 @@ internal sealed class ApplicationLauncher : IApplicationLauncher return Process.GetProcessesByName(ProcessName); } - public void KillGuildWarsProcess(Process process) + public void KillGuildWarsProcess(GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext) { - process.ThrowIfNull(); try { - if (process.StartInfo is not null && - process.StartInfo.FileName.Contains("Gw.exe", StringComparison.OrdinalIgnoreCase)) - { - process.Kill(true); - return; - } - + var process = guildWarsApplicationLaunchContext.GuildWarsProcess; if (process.MainModule?.FileName is not null && process.MainModule.FileName.Contains("Gw.exe", StringComparison.OrdinalIgnoreCase)) { @@ -418,12 +411,17 @@ internal sealed class ApplicationLauncher : IApplicationLauncher return; } } + catch (Win32Exception e) + { + this.logger.LogError(e, $"Insuficient privileges to kill GuildWars process with id {guildWarsApplicationLaunchContext.ProcessId}"); + this.privilegeManager.RequestAdminPrivileges("Insufficient privileges to kill Guild Wars process. Please restart as administrator and try again."); + } catch(Exception e) { - this.logger.LogError(e, $"Failed to kill GuildWars process with id {process?.Id}"); + this.logger.LogError(e, $"Failed to kill GuildWars process with id {guildWarsApplicationLaunchContext.ProcessId}"); this.notificationService.NotifyError( title: "Failed to kill GuildWars process", - description: $"Encountered exception while trying to kill GuildWars process with id {process?.Id}. Check logs for details"); + description: $"Encountered exception while trying to kill GuildWars process with id {guildWarsApplicationLaunchContext.ProcessId}. Check logs for details"); } } diff --git a/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs b/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs index 347565a3..fdc387ec 100644 --- a/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs +++ b/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs @@ -10,7 +10,7 @@ public interface IApplicationLauncher GuildWarsApplicationLaunchContext? GetGuildwarsProcess(LaunchConfigurationWithCredentials launchConfigurationWithCredentials); IEnumerable GetGuildwarsProcesses(params LaunchConfigurationWithCredentials[] launchConfigurationWithCredentials); IEnumerable GetGuildwarsProcesses(); - void KillGuildWarsProcess(Process process); + void KillGuildWarsProcess(GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext); Task LaunchGuildwars(LaunchConfigurationWithCredentials launchConfigurationWithCredentials); void RestartDaybreak(); void RestartDaybreakAsAdmin(); diff --git a/Daybreak/Services/GWCA/GWCAClient.cs b/Daybreak/Services/GWCA/GWCAClient.cs index 55ca2a0a..0ceb4dac 100644 --- a/Daybreak/Services/GWCA/GWCAClient.cs +++ b/Daybreak/Services/GWCA/GWCAClient.cs @@ -83,7 +83,7 @@ internal sealed class GWCAClient : IGWCAClient return new ConnectionContext(listener.Port, processId); } - catch (Exception e) when (e is TaskCanceledException or TimeoutException) + catch (Exception e) when (e is TaskCanceledException or TimeoutException or HttpRequestException) { scopedLogger.LogInformation($"Timed out trying to reach port. Continuing"); } diff --git a/Daybreak/Services/Injection/ProcessInjector.cs b/Daybreak/Services/Injection/ProcessInjector.cs index 9bc6781a..d4ac96e5 100644 --- a/Daybreak/Services/Injection/ProcessInjector.cs +++ b/Daybreak/Services/Injection/ProcessInjector.cs @@ -1,18 +1,16 @@ -using Daybreak.Utils; -using Microsoft.Extensions.Logging; -using System; +using Microsoft.Extensions.Logging; using System.Core.Extensions; using System.Diagnostics; using System.Extensions; using System.IO; -using System.Runtime.InteropServices; -using System.Text; using System.Threading; using System.Threading.Tasks; namespace Daybreak.Services.Injection; internal sealed class ProcessInjector : IProcessInjector { + private const string InjectorExe = "Daybreak.Injector.exe"; + private readonly ILogger logger; public ProcessInjector( @@ -23,136 +21,51 @@ internal sealed class ProcessInjector : IProcessInjector public Task Inject(Process process, string pathToDll, CancellationToken cancellationToken) { - return Task.Factory.StartNew(() => - { - return this.InjectWithWinApi(process, pathToDll); - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + return Task.Factory.StartNew(() => this.InjectWithInjector(process, pathToDll), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); } - - private bool InjectWithWinApi(Process process, string pathToDll) + + private bool InjectWithInjector(Process process, string pathToDll) { - var scopedLogger = this.logger.CreateScopedLogger(nameof(InjectWithWinApi), pathToDll); + var scopedLogger = this.logger.CreateScopedLogger(nameof(InjectWithInjector), pathToDll); var modulefullpath = Path.GetFullPath(pathToDll); - if (!File.Exists(modulefullpath)) { scopedLogger.LogError("Dll to inject not found"); return false; } - var hKernel32 = NativeMethods.GetModuleHandle("kernel32.dll"); - if (hKernel32 == IntPtr.Zero) + var injectorPath = Path.GetFullPath(InjectorExe); + if (!File.Exists(injectorPath)) { - scopedLogger.LogError("Unable to get a handle of kernel32.dll"); + scopedLogger.LogError("Could not find Daybreak injector"); return false; } - var hLoadLib = NativeMethods.GetProcAddress(hKernel32, "LoadLibraryW"); - if (hLoadLib == IntPtr.Zero) + var injectionProcess = new Process { - scopedLogger.LogError("Unable to get the address of LoadLibraryW"); - return false; + StartInfo = new ProcessStartInfo + { + Arguments = $"-p {process.Id} -d {pathToDll}", + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardInput = true, + RedirectStandardOutput = true, + FileName = injectorPath, + } + }; + + injectionProcess.Start(); + injectionProcess.WaitForExit(); + var result = injectionProcess.ExitCode; + if (result == 0) + { + scopedLogger.LogInformation("Injected into process"); + return true; } - var hStringBuffer = NativeMethods.VirtualAllocEx(process.Handle, IntPtr.Zero, new IntPtr(2 * (modulefullpath.Length + 1)), - 0x3000 /* MEM_COMMIT | MEM_RESERVE */, 0x4 /* PAGE_READWRITE */); - if (hStringBuffer == IntPtr.Zero) - { - scopedLogger.LogError("Unable to allocate memory for module path"); - return false; - } - - WriteWString(process, hStringBuffer, modulefullpath); - if (ReadWString(process, hStringBuffer, 260) != modulefullpath) - { - scopedLogger.LogError("Module path string is not correct"); - return false; - } - - var hThread = NativeMethods.CreateRemoteThread(process.Handle, IntPtr.Zero, 0, hLoadLib, hStringBuffer, 0, out _); - if (hThread == IntPtr.Zero) - { - scopedLogger.LogError("Unable to create remote thread"); - return false; - } - - var threadResult = NativeMethods.WaitForSingleObject(hThread, 30000u); - if (threadResult is 0x102 or 0xFFFFFFFF /* WAIT_FAILED */) - { - scopedLogger.LogError($"Exception occurred while waiting for the remote thread. Result is {threadResult}"); - return false; - } - - var dllResult = NativeMethods.GetExitCodeThread(hThread, out _); - if (dllResult == 0) - { - scopedLogger.LogError($"Injected dll returned non-success status code {dllResult}"); - return false; - } - - var memoryFreeResult = NativeMethods.VirtualFreeEx(process.Handle, hStringBuffer, 0, 0x8000 /* MEM_RELEASE */); - if (!memoryFreeResult) - { - scopedLogger.LogError($"Failed to free dll memory"); - } - - return memoryFreeResult; - } - - private static void WriteBytes(Process process, IntPtr address, byte[] data) - { - var size = data.Length; - var buffer = Marshal.AllocHGlobal(size); - Marshal.Copy(data, 0, buffer, size); - - NativeMethods.WriteProcessMemory( - process.Handle, - address, - buffer, - size, - out _); - - Marshal.FreeHGlobal(buffer); - } - - private static void WriteWString(Process process, IntPtr address, string data) - { - WriteBytes(process, address, Encoding.Unicode.GetBytes(data)); - } - - private static string ReadWString(Process process, IntPtr address, int maxsize, Encoding? encoding = null) - { - encoding ??= Encoding.Unicode; - var rawbytes = ReadBytes(process, address, maxsize); - if (rawbytes.Length == 0) - { - return ""; - } - - var ret = encoding.GetString(rawbytes); - if (ret.Contains('\0')) - { - ret = ret[..ret.IndexOf('\0')]; - } - - return ret; - } - - private static byte[] ReadBytes(Process process, IntPtr address, int size) - { - var buffer = Marshal.AllocHGlobal(size); - - NativeMethods.ReadProcessMemory(process.Handle, - address, - buffer, - size, - out _ - ); - - var ret = new byte[size]; - Marshal.Copy(buffer, ret, 0, size); - Marshal.FreeHGlobal(buffer); - - return ret; + var stdout = injectionProcess.StandardOutput.ReadToEnd(); + var stderr = injectionProcess.StandardError.ReadToEnd(); + scopedLogger.LogError($"Failed to inject. Details:\n{stdout}\n{stderr}"); + return false; } } diff --git a/Daybreak/Services/Scanner/GWCAMemoryReader.cs b/Daybreak/Services/Scanner/GWCAMemoryReader.cs index 38b621c5..313b00c4 100644 --- a/Daybreak/Services/Scanner/GWCAMemoryReader.cs +++ b/Daybreak/Services/Scanner/GWCAMemoryReader.cs @@ -65,15 +65,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "game", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "game", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var gameData = payload.Deserialize(); if (gameData is null) @@ -126,15 +126,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "inventory", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "inventory", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var inventoryData = payload.Deserialize(); if (inventoryData is null) @@ -171,15 +171,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "login", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "login", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var loginData = payload.Deserialize(); if (loginData is null) @@ -210,15 +210,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "game/mainplayer", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "game/mainplayer", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var playerData = payload.Deserialize(); if (playerData is null) @@ -248,15 +248,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var pathingPayload = payload.Deserialize(); if (pathingPayload is null) @@ -314,15 +314,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing/metadata", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing/metadata", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var pathingMetadataPayload = payload.Deserialize(); if (pathingMetadataPayload is null) @@ -352,15 +352,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "pregame", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "pregame", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var preGamePayload = payload.Deserialize(); if (preGamePayload is null) @@ -391,15 +391,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "session", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "session", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var sessionPayload = payload.Deserialize(); if (sessionPayload is null) @@ -443,15 +443,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "user", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "user", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var userPayload = payload.Deserialize(); if (userPayload is null) @@ -498,15 +498,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "map", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, "map", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var mapPayload = payload.Deserialize(); if (mapPayload is null) @@ -544,15 +544,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, $"game/state", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, $"game/state", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var gameStatePayload = payload.Deserialize(); if (gameStatePayload is null || @@ -600,15 +600,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, $"entities/name?id={entity.Id}", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, $"entities/name?id={entity.Id}", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var namePayload = payload.Deserialize(); if (namePayload is null || @@ -638,15 +638,15 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, $"items/name?id={id}&modifiers={string.Join(',', modifiers?.Select(m => m.ToString()) ?? Array.Empty())}", cancellationToken); - if (!response.IsSuccessStatusCode) - { - scopedLogger.LogError($"Received non-success response {response.StatusCode}"); - return default; - } - try { + var response = await this.client.GetAsync(this.connectionContextCache.Value, $"items/name?id={id}&modifiers={string.Join(',', modifiers?.Select(m => m.ToString()) ?? Array.Empty())}", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + var payload = await response.Content.ReadAsStringAsync(cancellationToken); var namePayload = payload.Deserialize(); if (namePayload is null || diff --git a/Daybreak/Views/FocusView.xaml.cs b/Daybreak/Views/FocusView.xaml.cs index 5122f9ef..8007b745 100644 --- a/Daybreak/Views/FocusView.xaml.cs +++ b/Daybreak/Views/FocusView.xaml.cs @@ -6,14 +6,17 @@ using Daybreak.Services.ApplicationLauncher; using Daybreak.Services.BuildTemplates; using Daybreak.Services.Experience; using Daybreak.Services.Navigation; +using Daybreak.Services.Notifications; using Daybreak.Services.Scanner; using Daybreak.Views.Trade; +using LiveChartsCore; using Microsoft.Extensions.Logging; using System; using System.Configuration; using System.Core.Extensions; using System.Extensions; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -29,10 +32,12 @@ public partial class FocusView : UserControl { private const string NamePlaceholder = "[NamePlaceholder]"; private const string WikiUrl = "https://wiki.guildwars.com/wiki/[NamePlaceholder]"; + private const int MaxRetries = 5; private static readonly TimeSpan UninitializedBackoff = TimeSpan.FromSeconds(15); private static readonly TimeSpan GameDataFrequency = TimeSpan.FromSeconds(1); + private readonly INotificationService notificationService; private readonly IBuildTemplateManager buildTemplateManager; private readonly IApplicationLauncher applicationLauncher; private readonly IGuildwarsMemoryCache guildwarsMemoryCache; @@ -83,6 +88,7 @@ public partial class FocusView : UserControl private CancellationTokenSource? cancellationTokenSource; public FocusView( + INotificationService notificationService, IBuildTemplateManager buildTemplateManager, IApplicationLauncher applicationLauncher, IGuildwarsMemoryCache guildwarsMemoryCache, @@ -91,6 +97,7 @@ public partial class FocusView : UserControl ILiveUpdateableOptions liveUpdateableOptions, ILogger logger) { + this.notificationService = notificationService.ThrowIfNull(); this.buildTemplateManager = buildTemplateManager.ThrowIfNull(); this.applicationLauncher = applicationLauncher.ThrowIfNull(); this.guildwarsMemoryCache = guildwarsMemoryCache.ThrowIfNull(); @@ -135,7 +142,7 @@ public partial class FocusView : UserControl return; } - var pathingMeta = await this.guildwarsMemoryCache.ReadPathingMetaData(this.cancellationTokenSource?.Token ?? CancellationToken.None); + var pathingMeta = await this.guildwarsMemoryCache.ReadPathingMetaData(this.cancellationTokenSource?.Token ?? CancellationToken.None) ?? throw new HttpRequestException(); if (pathingMeta?.TrapezoidCount == this.PathingData?.Trapezoids?.Count || this.cancellationTokenSource?.IsCancellationRequested is not false) { @@ -144,7 +151,7 @@ public partial class FocusView : UserControl } await this.Dispatcher.InvokeAsync(() => this.LoadingPathingData = true); - var maybePathingData = await this.guildwarsMemoryCache.ReadPathingData(this.cancellationTokenSource?.Token ?? CancellationToken.None); + var maybePathingData = await this.guildwarsMemoryCache.ReadPathingData(this.cancellationTokenSource?.Token ?? CancellationToken.None) ?? throw new HttpRequestException(); if (maybePathingData is not PathingData pathingData || pathingData.Trapezoids is null || pathingData.Trapezoids.Count == 0) @@ -162,6 +169,7 @@ public partial class FocusView : UserControl private async void PeriodicallyReadPathingData(CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.PeriodicallyReadPathingData), string.Empty); + var retries = 0; while (!cancellationToken.IsCancellationRequested) { try @@ -179,6 +187,7 @@ public partial class FocusView : UserControl await Task.WhenAll( this.UpdatePathingData(), Task.Delay(1000, cancellationToken)).ConfigureAwait(true); + retries = 0; } catch (InvalidOperationException ex) @@ -186,7 +195,7 @@ public partial class FocusView : UserControl scopedLogger.LogError(ex, "Encountered invalid operation exception. Cancelling periodic reading"); return; } - catch (Exception ex) when (ex is TimeoutException or OperationCanceledException) + catch (Exception ex) when (ex is TimeoutException or OperationCanceledException or HttpRequestException) { if (this.DataContext is not GuildWarsApplicationLaunchContext context) { @@ -200,8 +209,20 @@ public partial class FocusView : UserControl } catch (InvalidOperationException innerEx) { - scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); - await Task.Delay(UninitializedBackoff, cancellationToken); + retries++; + if (retries >= MaxRetries) + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Returning to launcher view"); + this.notificationService.NotifyError( + title: "GuildWars unresponsive", + description: "Could not connect to Guild Wars instance. Returning to Launcher view"); + this.viewManager.ShowView(); + } + else + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); + await Task.Delay(UninitializedBackoff, cancellationToken); + } } } catch (Exception ex) @@ -214,6 +235,7 @@ public partial class FocusView : UserControl private async void PeriodicallyReadGameState(CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.PeriodicallyReadGameState), string.Empty); + var retries = 0; while (!cancellationToken.IsCancellationRequested) { try @@ -233,21 +255,17 @@ public partial class FocusView : UserControl readGameStateTask, Task.Delay(16, cancellationToken)).ConfigureAwait(true); - var maybeGameState = await readGameStateTask; - if (maybeGameState is null) - { - continue; - } - + var maybeGameState = await readGameStateTask ?? throw new HttpRequestException(); this.GameState = maybeGameState; this.CanRotateMinimap = this.liveUpdateableOptions.Value.MinimapRotationEnabled; + retries = 0; } catch (InvalidOperationException ex) { scopedLogger.LogError(ex, "Encountered invalid operation exception. Cancelling periodic reading"); return; } - catch (Exception ex) when (ex is TimeoutException or OperationCanceledException) + catch (Exception ex) when (ex is TimeoutException or OperationCanceledException or HttpRequestException) { if (this.DataContext is not GuildWarsApplicationLaunchContext context) { @@ -261,8 +279,20 @@ public partial class FocusView : UserControl } catch (InvalidOperationException innerEx) { - scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); - await Task.Delay(UninitializedBackoff, cancellationToken); + retries++; + if (retries >= MaxRetries) + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Returning to launcher view"); + this.notificationService.NotifyError( + title: "GuildWars unresponsive", + description: "Could not connect to Guild Wars instance. Returning to Launcher view"); + this.viewManager.ShowView(); + } + else + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); + await Task.Delay(UninitializedBackoff, cancellationToken); + } } } catch (Exception ex) @@ -275,6 +305,7 @@ public partial class FocusView : UserControl private async void PeriodicallyReadGameData(CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.PeriodicallyReadGameData), string.Empty); + var retries = 0; while (!cancellationToken.IsCancellationRequested) { try @@ -294,7 +325,7 @@ public partial class FocusView : UserControl readGameDataTask, Task.Delay(GameDataFrequency, cancellationToken)).ConfigureAwait(true); - var maybeGameData = await readGameDataTask; + var maybeGameData = await readGameDataTask ?? throw new HttpRequestException(); if (maybeGameData is null) { continue; @@ -323,13 +354,14 @@ public partial class FocusView : UserControl } this.GameData = maybeGameData; + retries = 0; } catch (InvalidOperationException ex) { scopedLogger.LogError(ex, "Encountered invalid operation exception. Cancelling periodic reading"); return; } - catch (Exception ex) when (ex is TimeoutException or OperationCanceledException) + catch (Exception ex) when (ex is TimeoutException or OperationCanceledException or HttpRequestException) { if (this.DataContext is not GuildWarsApplicationLaunchContext context) { @@ -343,8 +375,20 @@ public partial class FocusView : UserControl } catch (InvalidOperationException innerEx) { - scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); - await Task.Delay(UninitializedBackoff, cancellationToken); + retries++; + if (retries >= MaxRetries) + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Returning to launcher view"); + this.notificationService.NotifyError( + title: "GuildWars unresponsive", + description: "Could not connect to Guild Wars instance. Returning to Launcher view"); + this.viewManager.ShowView(); + } + else + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); + await Task.Delay(UninitializedBackoff, cancellationToken); + } } } catch (Exception ex) @@ -357,6 +401,7 @@ public partial class FocusView : UserControl private async void PeriodicallyReadInventoryData(CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.PeriodicallyReadInventoryData), string.Empty); + var retries = 0; while (!cancellationToken.IsCancellationRequested) { try @@ -376,15 +421,16 @@ public partial class FocusView : UserControl readInventoryTask, Task.Delay(1000, cancellationToken)).ConfigureAwait(true); - var maybeInventoryData = await readInventoryTask; + var maybeInventoryData = await readInventoryTask ?? throw new HttpRequestException(); if (maybeInventoryData is null) { continue; } this.InventoryData = maybeInventoryData; + retries = 0; } - catch (Exception ex) when (ex is TimeoutException or OperationCanceledException) + catch (Exception ex) when (ex is TimeoutException or OperationCanceledException or HttpRequestException) { if (this.DataContext is not GuildWarsApplicationLaunchContext context) { @@ -398,8 +444,20 @@ public partial class FocusView : UserControl } catch (InvalidOperationException innerEx) { - scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); - await Task.Delay(UninitializedBackoff, cancellationToken); + retries++; + if (retries >= MaxRetries) + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Returning to launcher view"); + this.notificationService.NotifyError( + title: "GuildWars unresponsive", + description: "Could not connect to Guild Wars instance. Returning to Launcher view"); + this.viewManager.ShowView(); + } + else + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); + await Task.Delay(UninitializedBackoff, cancellationToken); + } } } catch (Exception ex) @@ -412,6 +470,7 @@ public partial class FocusView : UserControl private async void PeriodicallyReadMainPlayerContextData(CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.PeriodicallyReadMainPlayerContextData), string.Empty); + var retries = 0; while (!cancellationToken.IsCancellationRequested) { try @@ -443,9 +502,9 @@ public partial class FocusView : UserControl readMainPlayerDataTask, Task.Delay(16, cancellationToken)).ConfigureAwait(true); - var userData = await readUserDataTask; - var sessionData = await readSessionDataTask; - var mainPlayerData = await readMainPlayerDataTask; + var userData = await readUserDataTask ?? throw new HttpRequestException(); + var sessionData = await readSessionDataTask ?? throw new HttpRequestException(); + var mainPlayerData = await readMainPlayerDataTask ?? throw new HttpRequestException(); if (userData?.User is null || sessionData?.Session is null || mainPlayerData?.PlayerInformation is null || @@ -466,13 +525,14 @@ public partial class FocusView : UserControl this.MainPlayerDataValid = true; this.Browser.Visibility = this.minimapMaximized ? Visibility.Hidden : Visibility.Visible; + retries = 0; } catch (InvalidOperationException ex) { scopedLogger.LogError(ex, "Encountered invalid operation exception. Cancelling periodic main player reading"); return; } - catch (Exception ex) when (ex is TimeoutException or OperationCanceledException) + catch (Exception ex) when (ex is TimeoutException or OperationCanceledException or HttpRequestException) { if (this.DataContext is not GuildWarsApplicationLaunchContext context) { @@ -486,8 +546,20 @@ public partial class FocusView : UserControl } catch (InvalidOperationException innerEx) { - scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); - await Task.Delay(UninitializedBackoff, cancellationToken); + retries++; + if (retries >= MaxRetries) + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Returning to launcher view"); + this.notificationService.NotifyError( + title: "GuildWars unresponsive", + description: "Could not connect to Guild Wars instance. Returning to Launcher view"); + this.viewManager.ShowView(); + } + else + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); + await Task.Delay(UninitializedBackoff, cancellationToken); + } } } catch (Exception ex) diff --git a/Daybreak/Views/LauncherView.xaml.cs b/Daybreak/Views/LauncherView.xaml.cs index e16b897b..829da50e 100644 --- a/Daybreak/Views/LauncherView.xaml.cs +++ b/Daybreak/Views/LauncherView.xaml.cs @@ -101,7 +101,7 @@ public partial class LauncherView : UserControl { if (!this.launching) { - await this.Dispatcher.InvokeAsync(() => this.CanLaunch = this.LatestConfiguration?.CanLaunch ?? false); + await this.SetLaunchButtonState(); } await Task.Delay(TimeSpan.FromSeconds(1), this.cancellationTokenSource.Token); @@ -127,73 +127,110 @@ public partial class LauncherView : UserControl private async void DropDownButton_SelectionChanged(object _, object e) { - if (e is not LauncherViewContext context) + if (e is not LauncherViewContext) { return; } - if (this.LatestConfiguration is null || - this.LatestConfiguration.CanLaunch is false) - { - await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); - } - else - { - await this.Dispatcher.InvokeAsync(() => this.CanLaunch = true); - } + await this.SetLaunchButtonState(); } private async void DropDownButton_Clicked(object _, object e) { this.launching = true; await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); - var launchingTask = await new TaskFactory().StartNew(async () => + + if (this.LatestConfiguration.CanKill) { - var latestConfig = await this.Dispatcher.InvokeAsync(() => this.LatestConfiguration); - if (this.applicationLauncher.GetGuildwarsProcess(latestConfig.Configuration!) is GuildWarsApplicationLaunchContext context) - { - // Detected already running guildwars process - await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); - if (this.focusViewOptions.Value.Enabled) - { - this.menuService.CloseMenu(); - this.viewManager.ShowView(context); - } - - this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(latestConfig.Configuration!); - return; - } - + var killingTask = await new TaskFactory().StartNew(this.KillGuildWars, TaskCreationOptions.LongRunning); try { - var launchedContext = await this.applicationLauncher.LaunchGuildwars(latestConfig.Configuration!); - if (launchedContext is null) - { - await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); - return; - } - - this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(latestConfig.Configuration!); - if (this.focusViewOptions.Value.Enabled) - { - await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); - this.menuService.CloseMenu(); - this.viewManager.ShowView(launchedContext); - } + await killingTask; } - catch (Exception) + catch { } - }, TaskCreationOptions.LongRunning); - - try - { - await launchingTask; } - catch + else { + var launchingTask = await new TaskFactory().StartNew(this.LaunchGuildWars, TaskCreationOptions.LongRunning); + try + { + await launchingTask; + } + catch + { + } } this.launching = false; } + + private async Task SetLaunchButtonState() + { + if (this.LatestConfiguration is null) + { + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); + return; + } + + if (this.LatestConfiguration.CanKill is true) + { + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = true); + return; + } + + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = this.LatestConfiguration.CanLaunch); + } + + private async Task KillGuildWars() + { + var latestConfig = await this.Dispatcher.InvokeAsync(() => this.LatestConfiguration); + var context = this.applicationLauncher.GetGuildwarsProcess(latestConfig.Configuration!); + if (context is null) + { + return; + } + + this.applicationLauncher.KillGuildWarsProcess(context); + } + + private async Task LaunchGuildWars() + { + var latestConfig = await this.Dispatcher.InvokeAsync(() => this.LatestConfiguration); + if (this.applicationLauncher.GetGuildwarsProcess(latestConfig.Configuration!) is GuildWarsApplicationLaunchContext context) + { + // Detected already running guildwars process + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); + if (this.focusViewOptions.Value.Enabled) + { + this.menuService.CloseMenu(); + this.viewManager.ShowView(context); + } + + this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(latestConfig.Configuration!); + return; + } + + try + { + var launchedContext = await this.applicationLauncher.LaunchGuildwars(latestConfig.Configuration!); + if (launchedContext is null) + { + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); + return; + } + + this.launchConfigurationService.SetLastLaunchConfigurationWithCredentials(latestConfig.Configuration!); + if (this.focusViewOptions.Value.Enabled) + { + await this.Dispatcher.InvokeAsync(() => this.CanLaunch = false); + this.menuService.CloseMenu(); + this.viewManager.ShowView(launchedContext); + } + } + catch (Exception) + { + } + } } diff --git a/Daybreak/Views/LogsView.xaml.cs b/Daybreak/Views/LogsView.xaml.cs index 0ac0b479..d50ff7b3 100644 --- a/Daybreak/Views/LogsView.xaml.cs +++ b/Daybreak/Views/LogsView.xaml.cs @@ -10,6 +10,7 @@ using System.Extensions; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -31,6 +32,7 @@ public partial class LogsView : UserControl private readonly SimpleHighlightingBrush simpleOrangeHighlightingBrush; private readonly ILogsManager logManager; private readonly ILogger logger; + private readonly SemaphoreSlim semaphoreSlim = new(1, 1); private ScrollViewer? textEditorScrollViewer; @@ -56,8 +58,9 @@ public partial class LogsView : UserControl private async void UpdateLogs() { - var logs = this.logManager.GetLogs(l => l.LogLevel < LogLevel.Trace).ToArray(); + var logs = this.logManager.GetLogs().ToArray(); this.TextEditor.Clear(); + this.cachedText.Clear(); if (logs.Length > MaximumLookbackPeriod) { var maximumLookbackPeriodMessage = string.Format(MaximumLookbackMessageTemplate, MaximumLookbackPeriod); @@ -72,8 +75,9 @@ public partial class LogsView : UserControl private async Task WriteLogs(bool forceScrollToEnd, params Log[] logs) { - await this.Dispatcher.InvokeAsync(() => + await this.Dispatcher.InvokeAsync(async () => { + await this.semaphoreSlim.WaitAsync(); foreach (var log in logs) { var logTimeComponent = $"[{log.LogTime}]\t"; @@ -110,6 +114,8 @@ public partial class LogsView : UserControl { this.TextEditor.ScrollToEnd(); } + + this.semaphoreSlim.Release(); }); } @@ -148,6 +154,7 @@ public partial class LogsView : UserControl .GetField("scrollViewer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)? .GetValue(this.TextEditor)?.Cast(); this.logManager.ReceivedLog += this.LogManager_ReceivedLog; + this.UpdateLogs(); } private void LogsView_Unloaded(object sender, RoutedEventArgs _)