[WIP] Partially working process launching

Still need to iron out all the bugs and get it working with launchers etc.
This commit is contained in:
Terry MacDonald 2021-11-21 22:35:14 +13:00
parent 852d42ed6f
commit 0d96a9287d
5 changed files with 261 additions and 85 deletions

View File

@ -107,8 +107,11 @@ namespace DisplayMagician.GameLibraries
{ {
//if (gameProcess.MainModule.FileName.StartsWith(_steamGameExePath)) //if (gameProcess.MainModule.FileName.StartsWith(_steamGameExePath))
// numGameProcesses++; // numGameProcesses++;
if (gameProcess.ProcessName.Equals(_steamGameProcessName)) if (!gameProcess.HasExited)
{
numGameProcesses++; numGameProcesses++;
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -823,6 +823,9 @@ namespace DisplayMagician.GameLibraries
} }
//Process gameProcess = Process.Start(address); //Process gameProcess = Process.Start(address);
List<Process> gameProcesses = ProcessUtils.StartProcess(address,null,processPriority); List<Process> gameProcesses = ProcessUtils.StartProcess(address,null,processPriority);
// Wait 1 second then see if we need to find the child processes.
return gameProcesses; return gameProcesses;
} }
#endregion #endregion

View File

@ -56,16 +56,21 @@ namespace DisplayMagician.Processes
public static List<Process> StartProcess(string executable, string arguments, ProcessPriority processPriority, int startTimeout = 1) public static List<Process> StartProcess(string executable, string arguments, ProcessPriority processPriority, int startTimeout = 1)
{ {
List<Process> runningProcesses = new List<Process>(); List<Process> startedProcesses = new List<Process>();
Process process = null; List<Process> returnedProcesses;
bool usingChildProcess = false; //bool usingChildProcess = false;
if (TryExecute_AutoImpersonate(executable, arguments, out process)) if (TryExecute_AutoImpersonate(executable, arguments, out returnedProcesses))
{ {
logger.Trace($"ProcessUtils/StartProcess: {executable} {arguments} has successfully been started (ID: {process.Id})"); logger.Trace($"ProcessUtils/StartProcess: {executable} {arguments} has successfully been started");
runningProcesses.Add(process);
} }
return runningProcesses; else
{
logger.Trace($"ProcessUtils/StartProcess: {executable} {arguments} was unable to be started");
}
startedProcesses = returnedProcesses;
return startedProcesses;
} }
public static List<Process> GetChildProcesses(Process process) public static List<Process> GetChildProcesses(Process process)
@ -252,7 +257,7 @@ namespace DisplayMagician.Processes
/// <returns><c>true</c> if process was executed and finished correctly</returns> /// <returns><c>true</c> if process was executed and finished correctly</returns>
public static bool TryExecute(string executable, string arguments, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000) public static bool TryExecute(string executable, string arguments, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000)
{ {
Process unused; List<Process> unused;
return TryExecute(executable, arguments, out unused, priorityClass, maxWaitMs); return TryExecute(executable, arguments, out unused, priorityClass, maxWaitMs);
} }
@ -266,11 +271,19 @@ namespace DisplayMagician.Processes
/// <param name="priorityClass">Process priority</param> /// <param name="priorityClass">Process priority</param>
/// <param name="maxWaitMs">Maximum time to wait for completion</param> /// <param name="maxWaitMs">Maximum time to wait for completion</param>
/// <returns><c>true</c> if process was executed and finished correctly</returns> /// <returns><c>true</c> if process was executed and finished correctly</returns>
public static bool TryExecute_AutoImpersonate(string executable, string arguments, out Process process, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000) public static bool TryExecute_AutoImpersonate(string executable, string arguments, out List<Process> processes, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000)
{ {
return IsImpersonated ? bool result = false;
TryExecute_Impersonated(executable, arguments, out process, priorityClass, maxWaitMs) : if (IsImpersonated)
TryExecute(executable, arguments, out process, priorityClass, maxWaitMs); {
result = TryExecute_Impersonated(executable, arguments, out processes, priorityClass, maxWaitMs);
}
else
{
result = TryExecute(executable, arguments, out processes, priorityClass, maxWaitMs);
}
//process = processReturned;
return result;
} }
/// <summary> /// <summary>
@ -282,16 +295,16 @@ namespace DisplayMagician.Processes
/// <param name="priorityClass">Process priority</param> /// <param name="priorityClass">Process priority</param>
/// <param name="maxWaitMs">Maximum time to wait for completion</param> /// <param name="maxWaitMs">Maximum time to wait for completion</param>
/// <returns><c>true</c> if process was executed and finished correctly</returns> /// <returns><c>true</c> if process was executed and finished correctly</returns>
public static bool TryExecute_Impersonated(string executable, string arguments, out Process process, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000) public static bool TryExecute_Impersonated(string executable, string arguments, out List<Process> processes, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000)
{ {
IntPtr userToken; IntPtr userToken;
process = null; processes = new List<Process>();
if (!ImpersonationHelper.GetTokenByProcess(out userToken, true)) if (!ImpersonationHelper.GetTokenByProcess(out userToken, true))
return false; return false;
try try
{ {
//return TryExecute_Impersonated(executable, arguments, userToken, false, out unused, priorityClass, maxWaitMs); //return TryExecute_Impersonated(executable, arguments, userToken, false, out unused, priorityClass, maxWaitMs);
return TryExecute_Impersonated(executable, arguments, userToken, out process, priorityClass, maxWaitMs); return TryExecute_Impersonated(executable, arguments, userToken, out processes, priorityClass, maxWaitMs);
} }
finally finally
{ {
@ -309,7 +322,7 @@ namespace DisplayMagician.Processes
/// <param name="priorityClass">Process priority</param> /// <param name="priorityClass">Process priority</param>
/// <param name="maxWaitMs">Maximum time to wait for completion</param> /// <param name="maxWaitMs">Maximum time to wait for completion</param>
/// <returns></returns> /// <returns></returns>
public static bool TryExecuteReadString(string executable, string arguments, out Process process, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000) public static bool TryExecuteReadString(string executable, string arguments, out List<Process> process, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000)
{ {
return TryExecute(executable, arguments, out process, priorityClass, maxWaitMs); return TryExecute(executable, arguments, out process, priorityClass, maxWaitMs);
} }
@ -324,11 +337,11 @@ namespace DisplayMagician.Processes
/// <param name="priorityClass">Process priority</param> /// <param name="priorityClass">Process priority</param>
/// <param name="maxWaitMs">Maximum time to wait for completion</param> /// <param name="maxWaitMs">Maximum time to wait for completion</param>
/// <returns></returns> /// <returns></returns>
public static bool TryExecuteReadString_AutoImpersonate(string executable, string arguments, out Process process, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000) public static bool TryExecuteReadString_AutoImpersonate(string executable, string arguments, out List<Process> processes, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000)
{ {
return IsImpersonated ? return IsImpersonated ?
TryExecuteReadString_Impersonated(executable, arguments, out process, priorityClass, maxWaitMs) : TryExecuteReadString_Impersonated(executable, arguments, out processes, priorityClass, maxWaitMs) :
TryExecuteReadString(executable, arguments, out process, priorityClass, maxWaitMs); TryExecuteReadString(executable, arguments, out processes, priorityClass, maxWaitMs);
} }
/// <summary> /// <summary>
@ -342,17 +355,17 @@ namespace DisplayMagician.Processes
/// <param name="priorityClass">Process priority</param> /// <param name="priorityClass">Process priority</param>
/// <param name="maxWaitMs">Maximum time to wait for completion</param> /// <param name="maxWaitMs">Maximum time to wait for completion</param>
/// <returns><c>true</c> if process was executed and finished correctly</returns> /// <returns><c>true</c> if process was executed and finished correctly</returns>
public static bool TryExecuteReadString_Impersonated(string executable, string arguments, out Process process, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = INFINITE) public static bool TryExecuteReadString_Impersonated(string executable, string arguments, out List<Process> processes, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = INFINITE)
{ {
IntPtr userToken; IntPtr userToken;
if (!ImpersonationHelper.GetTokenByProcess(out userToken, true)) if (!ImpersonationHelper.GetTokenByProcess(out userToken, true))
{ {
process = null; processes = new List<Process>();
return false; return false;
} }
try try
{ {
return TryExecute_Impersonated(executable, arguments, userToken, out process, priorityClass, maxWaitMs); return TryExecute_Impersonated(executable, arguments, userToken, out processes, priorityClass, maxWaitMs);
} }
finally finally
{ {
@ -426,10 +439,10 @@ namespace DisplayMagician.Processes
/// <param name="priorityClass">Process priority</param> /// <param name="priorityClass">Process priority</param>
/// <param name="maxWaitMs">Maximum time to wait for completion</param> /// <param name="maxWaitMs">Maximum time to wait for completion</param>
/// <returns></returns> /// <returns></returns>
private static bool TryExecute(string executable, string arguments, out Process process, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000) private static bool TryExecute(string executable, string arguments, out List<Process> startedProcesses, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000)
{ {
//StringBuilder outputBuilder = new StringBuilder(); //StringBuilder outputBuilder = new StringBuilder();
startedProcesses = new List<Process>();
bool _isFile = false; bool _isFile = false;
bool _isExe = false; bool _isExe = false;
if (File.Exists(executable)) if (File.Exists(executable))
@ -469,44 +482,102 @@ namespace DisplayMagician.Processes
}; };
} }
using (Process processCreated = new Process { StartInfo = psi }) Process processCreated = new Process { StartInfo = psi };
/*if (redirectInputOutput)
{ {
/*if (redirectInputOutput) // Set UTF-8 encoding for standard output.
{ process.StartInfo.StandardOutputEncoding = CONSOLE_ENCODING;
// Set UTF-8 encoding for standard output. // Enable raising events because Process does not raise events by default.
process.StartInfo.StandardOutputEncoding = CONSOLE_ENCODING; process.EnableRaisingEvents = true;
// Enable raising events because Process does not raise events by default. // Attach the event handler for OutputDataReceived before starting the process.
process.EnableRaisingEvents = true; process.OutputDataReceived += (sender, e) => outputBuilder.Append(e.Data);
// Attach the event handler for OutputDataReceived before starting the process. }*/
process.OutputDataReceived += (sender, e) => outputBuilder.Append(e.Data);
}*/
try
{
processCreated.Start(); processCreated.Start();
/*if (redirectInputOutput)
process.BeginOutputReadLine();*/
if (processCreated.WaitForExit(maxWaitMs))
{
//result = RemoveEncodingPreamble(outputBuilder.ToString());
if (!processCreated.HasExited)
{
try
{
processCreated.PriorityClass = priorityClass;
}
catch(Exception ex)
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to set the Priority Class to {priorityClass.ToString("G")} for {executable}.");
}
process = processCreated;
return true;
}
}
} }
process = null; catch (ObjectDisposedException ex)
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. The process object has already been disposed.");
return false;
}
catch (InvalidOperationException ex)
{
if (processCreated.StartInfo.UseShellExecute && (processCreated.StartInfo.RedirectStandardInput || processCreated.StartInfo.RedirectStandardOutput || processCreated.StartInfo.RedirectStandardError))
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. The UseShellExecute member of the StartInfo property is true while RedirectStandardInput, RedirectStandardOutput, or RedirectStandardError is true.");
}
else
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. No file name was specified in the Process component's StartInfo.");
}
return false;
}
catch (Win32Exception ex)
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. There was an error in opening the associated file.");
return false;
}
catch (PlatformNotSupportedException ex)
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. Method not supported on operating systems without shell support such as Nano Server (.NET Core only).");
return false;
}
catch (Exception ex)
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. Not sure what specific exception it is.");
return false;
}
/*if (redirectInputOutput)
process.BeginOutputReadLine();*/
//result = RemoveEncodingPreamble(outputBuilder.ToString());
if (processCreated != null)
{
try
{
processCreated.WaitForExit(1000);
if (processCreated.HasExited)
{
// If the process has exited, then it's likely to be a launcher, so we try to find the children processes
List<Process> childProcesses = GetChildProcesses(processCreated);
startedProcesses.AddRange(childProcesses);
}
else
{
// If we're here then the process was created and hasn't exited!
try
{
if (processCreated.PriorityClass != priorityClass)
{
processCreated.PriorityClass = priorityClass;
}
}
catch (Exception ex)
{
logger.Warn(ex, $"ProcessUtils/StartProcess: Exception while trying to set the Priority Class to {priorityClass.ToString("G")} for {executable}.");
}
startedProcesses.Add(processCreated);
}
}
catch (Exception ex)
{
// Oops- the process didn't work. Let's skip this one
//process = null;
return false;
}
//process = processCreated;
return true;
}
//process = null;
return false; return false;
} }
@ -567,45 +638,105 @@ namespace DisplayMagician.Processes
/// <param name="priorityClass">Process priority</param> /// <param name="priorityClass">Process priority</param>
/// <param name="maxWaitMs">Maximum time to wait for completion</param> /// <param name="maxWaitMs">Maximum time to wait for completion</param>
/// <returns><c>true</c> if process was executed and finished correctly</returns> /// <returns><c>true</c> if process was executed and finished correctly</returns>
private static bool TryExecute_Impersonated(string executable, string arguments, IntPtr token, out Process process, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000) private static bool TryExecute_Impersonated(string executable, string arguments, IntPtr token, out List<Process> startedProcesses, ProcessPriorityClass priorityClass = ProcessPriorityClass.Normal, int maxWaitMs = 1000)
{ {
// TODO: code is 99% redundant to TryExecute, refactor Process/ImpersonationProcess and Start/StartAsUser! // TODO: code is 99% redundant to TryExecute, refactor Process/ImpersonationProcess and Start/StartAsUser!
StringBuilder outputBuilder = new StringBuilder(); StringBuilder outputBuilder = new StringBuilder();
using (ImpersonationProcess processCreated = new ImpersonationProcess { StartInfo = new ProcessStartInfo(executable, arguments) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = false} }) startedProcesses = new List<Process>();
ImpersonationProcess processCreated = new ImpersonationProcess { StartInfo = new ProcessStartInfo(executable, arguments) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = false } };
/*if (redirectInputOutput)
{
// Set UTF-8 encoding for standard output.
process.StartInfo.StandardOutputEncoding = CONSOLE_ENCODING;
// Enable raising events because Process does not raise events by default.
process.EnableRaisingEvents = true;
// Attach the event handler for OutputDataReceived before starting the process.
process.OutputDataReceived += (sender, e) => outputBuilder.Append(e.Data);
}*/
try
{ {
/*if (redirectInputOutput)
{
// Set UTF-8 encoding for standard output.
process.StartInfo.StandardOutputEncoding = CONSOLE_ENCODING;
// Enable raising events because Process does not raise events by default.
process.EnableRaisingEvents = true;
// Attach the event handler for OutputDataReceived before starting the process.
process.OutputDataReceived += (sender, e) => outputBuilder.Append(e.Data);
}*/
processCreated.StartAsUser(token); processCreated.StartAsUser(token);
}
/*if (redirectInputOutput) catch (ObjectDisposedException ex)
process.BeginOutputReadLine();*/ {
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. The process object has already been disposed.");
if (processCreated.WaitForExit(maxWaitMs)) return false;
}
catch (InvalidOperationException ex)
{
if (processCreated.StartInfo.UseShellExecute && (processCreated.StartInfo.RedirectStandardInput || processCreated.StartInfo.RedirectStandardOutput || processCreated.StartInfo.RedirectStandardError))
{ {
//result = RemoveEncodingPreamble(outputBuilder.ToString()); logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. The UseShellExecute member of the StartInfo property is true while RedirectStandardInput, RedirectStandardOutput, or RedirectStandardError is true.");
if (!processCreated.HasExited) }
else
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. No file name was specified in the Process component's StartInfo.");
}
return false;
}
catch (Win32Exception ex)
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. There was an error in opening the associated file.");
return false;
}
catch (PlatformNotSupportedException ex)
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. Method not supported on operating systems without shell support such as Nano Server (.NET Core only).");
return false;
}
catch (Exception ex)
{
logger.Error(ex, $"ProcessUtils/TryExecute: Exception while trying to start {executable}. Not sure what specific exception it is.");
return false;
}
/*if (redirectInputOutput)
process.BeginOutputReadLine();*/
if (processCreated != null)
{
try
{
processCreated.WaitForExit(1000);
if (processCreated.HasExited)
{ {
// If the process has exited, then it's likely to be a launcher, so we try to find the children processes
List<Process> childProcesses = GetChildProcesses(processCreated);
startedProcesses.AddRange(childProcesses);
}
else
{
// If we're here then the process was created and hasn't exited!
try try
{ {
processCreated.PriorityClass = priorityClass; if (processCreated.PriorityClass != priorityClass)
{
processCreated.PriorityClass = priorityClass;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex, $"ProcessUtils/TryExecute_Impersonated: Exception while trying to set the Priority Class to {priorityClass.ToString("G")} for {executable}."); logger.Warn(ex, $"ProcessUtils/StartProcess: Exception while trying to set the Priority Class to {priorityClass.ToString("G")} for {executable}.");
} }
process = processCreated; startedProcesses.Add(processCreated);
return true;
} }
} }
catch (Exception ex)
{
// Oops- the process didn't work. Let's skip this one
//process = null;
return false;
}
//process = processCreated;
return true;
} }
process = null;
return false; return false;
} }
@ -621,6 +752,33 @@ namespace DisplayMagician.Processes
return rawString; return rawString;
} }
public static ProcessPriorityClass TranslatePriorityToClass(ProcessPriority processPriorityClass)
{
ProcessPriorityClass wantedPriorityClass = ProcessPriorityClass.Normal;
switch (processPriorityClass)
{
case ProcessPriority.High:
wantedPriorityClass = ProcessPriorityClass.High;
break;
case ProcessPriority.AboveNormal:
wantedPriorityClass = ProcessPriorityClass.AboveNormal;
break;
case ProcessPriority.Normal:
wantedPriorityClass = ProcessPriorityClass.Normal;
break;
case ProcessPriority.BelowNormal:
wantedPriorityClass = ProcessPriorityClass.BelowNormal;
break;
case ProcessPriority.Idle:
wantedPriorityClass = ProcessPriorityClass.Idle;
break;
default:
wantedPriorityClass = ProcessPriorityClass.Normal;
break;
}
return wantedPriorityClass;
}
public static PROCESS_CREATION_FLAGS TranslatePriorityClassToFlags(ProcessPriorityClass processPriorityClass) public static PROCESS_CREATION_FLAGS TranslatePriorityClassToFlags(ProcessPriorityClass processPriorityClass)
{ {
PROCESS_CREATION_FLAGS wantedPriorityClass = PROCESS_CREATION_FLAGS.NORMAL_PRIORITY_CLASS; PROCESS_CREATION_FLAGS wantedPriorityClass = PROCESS_CREATION_FLAGS.NORMAL_PRIORITY_CLASS;

View File

@ -26,8 +26,8 @@ using System.Resources;
[assembly: Guid("e4ceaf5e-ad01-4695-b179-31168eb74c48")] [assembly: Guid("e4ceaf5e-ad01-4695-b179-31168eb74c48")]
// Version information // Version information
[assembly: AssemblyVersion("2.1.0.287")] [assembly: AssemblyVersion("2.1.0.308")]
[assembly: AssemblyFileVersion("2.1.0.287")] [assembly: AssemblyFileVersion("2.1.0.308")]
[assembly: NeutralResourcesLanguageAttribute( "en" )] [assembly: NeutralResourcesLanguageAttribute( "en" )]
[assembly: CLSCompliant(true)] [assembly: CLSCompliant(true)]

View File

@ -1276,6 +1276,15 @@ namespace DisplayMagician
// Delay 500ms // Delay 500ms
Thread.Sleep(500); Thread.Sleep(500);
if (gameProcesses.Count == 0)
{
// If there are no children found, then try to find all the running programs with the same names
// (Some games relaunch themselves!)
List<Process> sameNamedProcesses = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(gameToRun.Executable)).ToList();
gameProcesses.AddRange(sameNamedProcesses);
}
// Wait for GameLibrary to start // Wait for GameLibrary to start
for (int secs = 0; secs <= (shortcutToUse.StartTimeout * 1000); secs += 500) for (int secs = 0; secs <= (shortcutToUse.StartTimeout * 1000); secs += 500)
{ {
@ -1659,12 +1668,14 @@ namespace DisplayMagician
// Now we know the game library app is running then // Now we know the game library app is running then
// we wait until the game has started running (*allows for updates to occur) // we wait until the game has started running (*allows for updates to occur)
bool gameRunning = false;
for (int secs = 0; secs <= (shortcutToUse.StartTimeout * 1000); secs += 500) for (int secs = 0; secs <= (shortcutToUse.StartTimeout * 1000); secs += 500)
{ {
if (gameToRun.IsRunning) if (gameToRun.IsRunning)
{ {
// The game is running! So now we continue processing // The game is running! So now we continue processing
gameRunning = true;
logger.Debug($"ShortcutRepository/RunShortcut: Found the '{gameToRun.Name}' process has started"); logger.Debug($"ShortcutRepository/RunShortcut: Found the '{gameToRun.Name}' process has started");
try try
@ -1683,13 +1694,14 @@ namespace DisplayMagician
break; break;
} }
// Delay 500ms // Delay 500ms
Thread.Sleep(500); Thread.Sleep(500);
} }
// If the game still isn't running then there is an issue so tell the user and revert things back // If the game still isn't running then there is an issue so tell the user and revert things back
if (!gameToRun.IsRunning) if (!gameRunning)
{ {
logger.Error($"ShortcutRepository/RunShortcut: The Game {gameToRun.Name} didn't start for some reason (or the game uses a starter exe that launches the game itself)! so reverting changes back if needed..."); logger.Error($"ShortcutRepository/RunShortcut: The Game {gameToRun.Name} didn't start for some reason (or the game uses a starter exe that launches the game itself)! so reverting changes back if needed...");
logger.Warn($"ShortcutRepository/RunShortcut: We were monitoring {gameToRun.ExePath}. You may need to manually add an alternative game executable to monitor - please run the game manually and check if another executable in {Path.GetDirectoryName(gameToRun.ExePath)} is run, and then monitor that instead."); logger.Warn($"ShortcutRepository/RunShortcut: We were monitoring {gameToRun.ExePath}. You may need to manually add an alternative game executable to monitor - please run the game manually and check if another executable in {Path.GetDirectoryName(gameToRun.ExePath)} is run, and then monitor that instead.");