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 _)