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
This commit is contained in:
2024-01-31 23:02:14 +01:00
committed by GitHub
parent 8d8a78cf88
commit 8ee7ed23e4
19 changed files with 1111 additions and 327 deletions
+6 -1
View File
@@ -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
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
matrix:
targetplatform: [x86]
targetplatform: [x64]
runs-on: windows-latest
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Daybreak.Injector</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x86</Platforms>
<PublishSingleFile>True</PublishSingleFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="SystemExtensions.NetCore" Version="1.0.1" />
<PackageReference Include="SystemExtensions.NetStandard" Version="1.6.2" />
</ItemGroup>
</Project>
+469
View File
@@ -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);
}
+141
View File
@@ -0,0 +1,141 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace Daybreak.Injector;
internal static class ProcessInjector
{
public static Task<bool> 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;
}
}
+63
View File
@@ -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;
+18 -4
View File
@@ -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
@@ -19,7 +19,7 @@
</UserControl.Resources>
<Grid>
<StackPanel
Visibility="{Binding ElementName=_this, Path=CanShowFocusView, Mode=OneWay, Converter={StaticResource InverseBooleanToVisibilityConverter}}">
Visibility="{Binding ElementName=_this, Path=CanLaunch, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<WrapPanel VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock Text="Launch "
@@ -29,10 +29,6 @@
<TextBlock Text="{Binding Configuration.Credentials.Username}"
FontSize="22"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"/>
<TextBlock Text=" running"
FontSize="22"
Foreground="{StaticResource MahApps.Brushes.Accent}"
Visibility="{Binding ElementName=_this, Path=GameRunning, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</WrapPanel>
<TextBlock Text="{Binding Configuration.ExecutablePath}"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
@@ -41,7 +37,7 @@
HorizontalAlignment="Center"/>
</StackPanel>
<StackPanel
Visibility="{Binding ElementName=_this, Path=CanShowFocusView, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
Visibility="{Binding ElementName=_this, Path=CanAttach, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<WrapPanel VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock Text="Attach "
@@ -57,5 +53,22 @@
TextWrapping="Wrap"
HorizontalAlignment="Center"/>
</StackPanel>
<StackPanel
Visibility="{Binding ElementName=_this, Path=CanKill, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<WrapPanel VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock Text="Kill "
FontSize="22"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}" />
<TextBlock Text="{Binding Configuration.Credentials.Username}"
FontSize="22"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"/>
</WrapPanel>
<TextBlock Text="{Binding Configuration.ExecutablePath}"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
FontSize="12"
TextWrapping="Wrap"
HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
</UserControl>
@@ -31,8 +31,14 @@ public partial class LaunchButtonTemplate : UserControl
private readonly IApplicationLauncher applicationLauncher;
private readonly ILiveOptions<FocusViewOptions> 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;
}
}
+1 -1
View File
@@ -11,7 +11,7 @@
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
<Version>0.9.9.11</Version>
<Version>0.9.9.12</Version>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
+12
View File
@@ -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)));
}
}
}
@@ -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<Process?> LaunchGuildwarsProcess(string email, Models.SecureString password, string executable)
private async Task<Process?> 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<LauncherView>("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");
}
}
@@ -10,7 +10,7 @@ public interface IApplicationLauncher
GuildWarsApplicationLaunchContext? GetGuildwarsProcess(LaunchConfigurationWithCredentials launchConfigurationWithCredentials);
IEnumerable<GuildWarsApplicationLaunchContext?> GetGuildwarsProcesses(params LaunchConfigurationWithCredentials[] launchConfigurationWithCredentials);
IEnumerable<Process> GetGuildwarsProcesses();
void KillGuildWarsProcess(Process process);
void KillGuildWarsProcess(GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext);
Task<GuildWarsApplicationLaunchContext?> LaunchGuildwars(LaunchConfigurationWithCredentials launchConfigurationWithCredentials);
void RestartDaybreak();
void RestartDaybreakAsAdmin();
+1 -1
View File
@@ -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");
}
+33 -120
View File
@@ -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<ProcessInjector> logger;
public ProcessInjector(
@@ -23,136 +21,51 @@ internal sealed class ProcessInjector : IProcessInjector
public Task<bool> 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;
}
}
+91 -91
View File
@@ -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<GameDataPayload>();
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<InventoryPayload>();
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<LoginPayload>();
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<MainPlayerPayload>();
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<PathingPayload>();
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<PathingMetadataPayload>();
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<PreGamePayload>();
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<SessionPayload>();
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<UserPayload>();
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<MapPayload>();
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<GameStatePayload>();
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<NamePayload>();
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<string>())}", 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<string>())}", 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<NamePayload>();
if (namePayload is null ||
+100 -28
View File
@@ -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<FocusViewOptions> liveUpdateableOptions,
ILogger<FocusView> 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<LauncherView>();
}
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<LauncherView>();
}
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<LauncherView>();
}
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<LauncherView>();
}
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<LauncherView>();
}
else
{
scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying");
await Task.Delay(UninitializedBackoff, cancellationToken);
}
}
}
catch (Exception ex)
+85 -48
View File
@@ -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<FocusView>(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<FocusView>(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<FocusView>(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<FocusView>(launchedContext);
}
}
catch (Exception)
{
}
}
}
+9 -2
View File
@@ -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<LogsView> 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<ScrollViewer>();
this.logManager.ReceivedLog += this.LogManager_ReceivedLog;
this.UpdateLogs();
}
private void LogsView_Unloaded(object sender, RoutedEventArgs _)