JD2022-TU1/main/extern/UbiServices/upstream/external/tools/xboxLauncher/XboxLauncher/Program.cs

607 lines
24 KiB
C#

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Text;
using static XboxLauncher.ErrorCodes;
using static XboxLauncher.Helpers;
namespace XboxLauncher
{
class Program
{
enum SdkType
{
GDK,
XDK
}
static readonly object key = new object();
static readonly string succesString = "Ubiservices Tests have come to a completion with success.";
static readonly string failureString = "Ubiservices Tests have come to a completion with failure.";
static int deploymentTimeoutMilliseconds = 600 * 1000;
static Process outProcess = new Process();
static bool isDebugStringSingleTestDone = false;
static bool overallTestsSuccess = true;
static Thread watchDogThread;
static SdkType sdkType;
static string sdkPath = "";
static string xbConnectExe = "";
static string xbAppExe = "";
static string xbdbgmonExe = "";
static string xbtlistExe = "";
static bool isRunning = false;
static readonly object isRunningLock = new object();
static readonly long maxFindPidMilliseconds = 15000; // 15 seconds
static readonly long maxDisplayPidNotFoundMilliseconds = 5000; // 5 seconds
static readonly int resumeAppWaitTime = 2000; // 2 seconds
private static void InitializeSdkTools()
{
if (sdkType == SdkType.GDK)
{
sdkPath = Environment.GetEnvironmentVariable("GameDK");
}
else
{
sdkPath = Environment.GetEnvironmentVariable("DurangoXDK");
}
xbConnectExe = sdkPath + "bin\\xbconnect.exe";
xbAppExe = sdkPath + "bin\\xbapp.exe";
xbdbgmonExe = sdkPath + "bin\\xbdbgmon.exe";
xbtlistExe = sdkPath + "bin\\xbtlist.exe";
}
private static void SortOutputHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
if (!String.IsNullOrEmpty(outLine.Data))
{
// formatting the output.
if (outLine.Data != "(null)" && !outLine.Data.Contains("Look up CLSID"))
{
if (outLine.Data.Contains("[ ]") ||
outLine.Data.Contains("[----------]") ||
outLine.Data.Contains("[==========]") ||
outLine.Data.Contains("[ PASSED ]") ||
outLine.Data.Contains("[ RUN ]"))
{
OutputText(outLine.Data + "\n");
}
else if (outLine.Data.Contains("[ OK ]") ||
outLine.Data.Contains("[ FAILED ]"))
{
isDebugStringSingleTestDone = true;
OutputText(outLine.Data + "\n");
}
else
{
if (isDebugStringSingleTestDone)
{
OutputText(outLine.Data + "\n");
isDebugStringSingleTestDone = false;
}
else
{
OutputText(outLine.Data + "\n");
}
}
}
if (outLine.Data.Contains(failureString))
{
overallTestsSuccess = false;
}
// The process xbdbgmon hangs the cmd until it gets killed(this is MS design).
// MS doc: Use Ctrl+C to terminate monitoring(AKA kill it).
if (outLine.Data.Contains(succesString) || outLine.Data.Contains(failureString))
{
outProcess.CancelOutputRead();
outProcess.Kill();
}
}
}
private static int doFindPid(string appProcessName, bool displayErrorOutput)
{
string pid = "";
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = xbtlistExe;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
startInfo.Arguments = "/X/title";
try
{
using (Process exeProcess = Process.Start(startInfo))
{
exeProcess.WaitForExit();
string pidOutput = exeProcess.StandardOutput.ReadToEnd();
Regex pidRegex = new Regex(@"(\d+) .*\\" + appProcessName);
Match pidMatch = pidRegex.Match(pidOutput);
if (pidMatch.Success)
{
pid = pidMatch.Groups[1].ToString();
}
else
{
if(displayErrorOutput)
{
OutputText("\nFind PID Error: \n" + pidOutput + "\nRetrying...\n", ConsoleColor.Yellow);
}
return -1;
}
}
}
catch (Exception e)
{
OutputText("\nA problem occurred when finding the PID.\n", ConsoleColor.Red);
OutputText(e.ToString(), ConsoleColor.Red);
return -1;
}
}
return Int32.Parse(pid);
}
private static int findPid(string appProcessName)
{
// Note:
// During the app launch on the console, the xbapp tool starts the title asynchronously and in some
// cases it takes some time until the title is actually launched and the list of running titles is updated.
// As a result, if the doFindPid method is called too soon, the xblist process (used internally) might return a
// "Title is not running on target console..." output and the launcher will exit too soon.
int pid = doFindPid(appProcessName, true);
Stopwatch findPidChrono = new Stopwatch();
findPidChrono.Start();
// Stopwatch used to avoid spamming the logs with the Pid not found error
Stopwatch displayPidNotFoundErrorChrono = new Stopwatch();
displayPidNotFoundErrorChrono.Start();
// Retry to find the pid with a timeout
while (pid == -1 && findPidChrono.ElapsedMilliseconds < maxFindPidMilliseconds)
{
bool displayPidNotFound = displayPidNotFoundErrorChrono.ElapsedMilliseconds >= maxDisplayPidNotFoundMilliseconds;
if (displayPidNotFound)
{
displayPidNotFoundErrorChrono.Restart();
}
pid = doFindPid(appProcessName, displayPidNotFound);
}
return pid;
}
// Launches the application on the console.
// On GDK it launches the application without starting the execution (defered mode) to allow the debugger attachment to capture the logs.
// On XDK the defered mode is not available and the app will be started normally.
private static void LaunchApplication(string appName, string appArgs)
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = xbAppExe;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
if (sdkType == SdkType.GDK)
{
startInfo.Arguments = "launch /DeferExecution " + appName + " " + appArgs;
}
else
{
startInfo.Arguments = "launch " + appName + " " + appArgs;
}
try
{
if (sdkType == SdkType.GDK)
{
OutputText("\nLaunching the application in defered mode with args: " + appArgs + "\n", ConsoleColor.Cyan);
}
else
{
OutputText("\nLaunching the application with args: " + appArgs + "\n", ConsoleColor.Cyan);
}
using (Process exeProcess = Process.Start(startInfo))
{
exeProcess.WaitForExit();
OutputText(exeProcess.StandardOutput.ReadToEnd(), ConsoleColor.Blue);
}
}
catch (Exception e)
{
Exit(ERR_APPLICATION_LAUNCH_FAILURE, e.ToString());
}
}
// Method used to resume the app when the Launch with defered mode is used on GDK
private static void ResumeApplicationAsync()
{
new Thread(() =>
{
// Wait a bit to ensure that the xbdbgmon connected properly
// and finished any possible internal async tasks.
// Note: There were noticed cases where the first few lines
// of output were missing.
Thread.Sleep(resumeAppWaitTime);
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = xbAppExe;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
startInfo.Arguments = "continueExecution";
try
{
OutputText("\nResuming the application...", ConsoleColor.Cyan);
using (Process exeProcess = Process.Start(startInfo))
{
exeProcess.WaitForExit();
OutputText(exeProcess.StandardOutput.ReadToEnd(), ConsoleColor.Blue);
}
}
catch (Exception e)
{
Exit(ERR_APPLICATION_RESUME_FAILURE, e.ToString());
}
}).Start();
}
private static void WatchDog(Object appProcessNameObject)
{
string appProcessName;
try
{
appProcessName = (string)appProcessNameObject;
}
catch (InvalidCastException)
{
OutputText("\n\n Invalid app name object sent to the WatchDog thread\n\n", ConsoleColor.Red);
outProcess.CancelOutputRead();
outProcess.Kill();
return;
}
bool running;
lock (isRunningLock)
{
running = isRunning;
}
while (true && running)
{
Thread.Sleep(15000);
int pid = findPid(appProcessName);
if (pid < 0)
{
OutputText("\n\nCouldn't find " + appProcessName + " running on XboxOne. Exiting.\n\n", ConsoleColor.Red);
try
{
outProcess.CancelOutputRead();
outProcess.Kill();
}
catch (Exception)
{
//That means the executable ended normally.
}
finally
{
Exit(ERR_APPLICATION_PID_NOT_FOUND);
}
}
lock (isRunningLock)
{
running = isRunning;
}
}
}
static void Main(string[] args)
{
//setting the path where the exe is found.
const string exeFilePathArg = "-exeFilePath=";
const string packageNameArg = "-packageName=";
const string sdkTypeArg = "-sdkType=";
if (args.Length < 3 || !args[0].StartsWith(exeFilePathArg) || !args[1].StartsWith(packageNameArg) || !args[2].StartsWith(sdkTypeArg))
{
Exit(ERR_ARGUMENTS_INVALID, GetUsageMessage());
}
// Setting the executable path, app bundle and process name
string xOneExeFilePath = args[0].Substring(exeFilePathArg.Length);
if (!File.Exists(xOneExeFilePath))
{
Exit(ERR_ARGUMENTS_EXE_FILE_PATH_INVALID, "Given value: " + xOneExeFilePath);
}
string xOneAppBundle = Path.GetDirectoryName(xOneExeFilePath);
OutputText("\nAppBundlePath : " + xOneAppBundle, ConsoleColor.Blue);
string xOneAppProcessName = Path.GetFileName(xOneExeFilePath);
OutputText("\nAppProcessName : " + xOneAppProcessName, ConsoleColor.Blue);
// Setting the package name
string xOnePackageName = args[1].Substring(packageNameArg.Length);
// Setting the used SDK type (XDK/GDK)
string sdkTypeString = args[2].Substring(sdkTypeArg.Length);
if (!Enum.TryParse(sdkTypeString, true, out sdkType))
{
Exit(ERR_ARGUMENTS_SDK_TYPE_INVALID, "Given value: " + sdkTypeString);
}
// Initializing the SDK tools depending on the used SDK type
InitializeSdkTools();
// Setting the application arguments
string xOneApplicationArgs = "";
for (int i = 3; i < args.Length; i++)
{
xOneApplicationArgs += args[i] + " ";
}
OutputText("\nConnecting to the default console.", ConsoleColor.Blue);
//connection to the console
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = xbConnectExe;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
try
{
using (Process exeProcess = Process.Start(startInfo))
{
exeProcess.WaitForExit();
OutputText(exeProcess.StandardOutput.ReadToEnd(), ConsoleColor.Blue);
}
}
catch (Exception e)
{
Exit(ERR_CONSOLE_CONNECTION, e.ToString());
}
}
//Finding full package name
string fullPackageName = "";
{
OutputText("\nFinding if package is already installed\n", ConsoleColor.Blue);
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = xbAppExe;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
startInfo.Arguments = "list";
try
{
using (Process exeProcess = Process.Start(startInfo))
{
string packageListOutput = exeProcess.StandardOutput.ReadToEnd();
Regex packageRegex = new Regex(@"(" + xOnePackageName + ".+)");
Match packageMatch = packageRegex.Match(packageListOutput);
if (packageMatch.Success)
{
fullPackageName = packageMatch.Groups[1].ToString().Trim();
}
}
}
catch (Exception e)
{
OutputText(e.ToString(), ConsoleColor.Red);
OutputText("\n An error occured while listing installed app, not critical, continuing\n", ConsoleColor.Green);
}
}
//Uninstalling if installed
if (fullPackageName.Length > 0)
{
OutputText("\nUninstalling currently installed package\n", ConsoleColor.Blue);
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = xbAppExe;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
startInfo.Arguments = "uninstall " + fullPackageName;
try
{
using (Process exeProcess = Process.Start(startInfo))
{
exeProcess.WaitForExit();
OutputText(exeProcess.StandardOutput.ReadToEnd(), ConsoleColor.Blue);
}
}
catch (Exception e)
{
OutputText(e.ToString(), ConsoleColor.Red);
OutputText("\n An error occured while uninstalling app. Will attempt to overwrite app.\n", ConsoleColor.Green);
}
}
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
// deployment of the file to the console.
{
OutputText("\nDeploying app package\n", ConsoleColor.Blue);
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = xbAppExe;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.Arguments = "deploy " + xOneAppBundle + " /s /v";
try
{
using (Process exeProcess = new Process())
{
// Sometimes the deployment process hangs because of the internal buffer as described in the following stackoverflow issue:
// https://stackoverflow.com/questions/139593/processstartinfo-hanging-on-waitforexit-why
// The solution is to use asynchronous reads to assure that the buffer doesn't get full.
using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
exeProcess.OutputDataReceived += (sender, e) => {
if (e.Data == null)
{
outputWaitHandle.Set();
}
else
{
output.AppendLine(e.Data);
OutputText("\n" + e.Data, ConsoleColor.Magenta);
}
};
exeProcess.ErrorDataReceived += (sender, e) =>
{
if (e.Data == null)
{
errorWaitHandle.Set();
}
else
{
error.AppendLine(e.Data);
OutputText("\n" + e.Data, ConsoleColor.DarkRed);
}
};
exeProcess.StartInfo = startInfo;
exeProcess.Start();
exeProcess.BeginOutputReadLine();
exeProcess.BeginErrorReadLine();
if (exeProcess.WaitForExit(deploymentTimeoutMilliseconds) &&
outputWaitHandle.WaitOne(deploymentTimeoutMilliseconds) &&
errorWaitHandle.WaitOne(deploymentTimeoutMilliseconds))
{
OutputText("\nDeployment completed \n", ConsoleColor.Green);
}
else
{
OutputText("\nDeployment timeout\n", ConsoleColor.Red);
}
}
}
}
catch (Exception e)
{
Exit(ERR_APPLICATION_DEPLOY_FAILURE, e.ToString());
}
}
// This gets the name of the application that needs to be launched.
// On XDK the application name is appended by "!App"
// On GDK the application name is appeneded by "!Game"
string regexString = "";
if (sdkType == SdkType.XDK)
{
regexString = "([a-z]|[0-9]|\\.|-|_)+!App";
}
else if (sdkType == SdkType.GDK)
{
regexString = "([a-z]|[0-9]|\\.|-|_)+!Game";
}
Regex regex = new Regex(regexString, RegexOptions.IgnoreCase);
Match match = regex.Match(output.ToString());
string appName = "";
if (match.Success)
{
appName = match.Captures[0].ToString();
OutputText("\nAppName to deploy: " + appName, ConsoleColor.Blue);
}
else
{
Exit(ERR_APPLICATION_NAME_NOT_FOUND);
}
LaunchApplication(appName, xOneApplicationArgs);
OutputText("\nFinding PID of " + appName + " \n");
string pid = findPid(xOneAppProcessName).ToString();
if (pid == "-1")
{
Exit(ERR_APPLICATION_PID_NOT_FOUND);
}
OutputText("\nPID of the process is: " + pid.ToString() + "\n", ConsoleColor.Blue);
isRunning = true;
{
watchDogThread = new Thread(WatchDog);
watchDogThread.Start(xOneAppProcessName);
}
//Launches the process that captures the debug information
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = xbdbgmonExe;
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardOutput = true;
startInfo.Arguments = pid;
try
{
outProcess.StartInfo = startInfo;
outProcess.OutputDataReceived += new DataReceivedEventHandler(SortOutputHandler);
outProcess.Start();
outProcess.BeginOutputReadLine();
if (sdkType == SdkType.GDK)
{
ResumeApplicationAsync();
}
outProcess.WaitForExit();
watchDogThread.Abort();
if(!overallTestsSuccess)
{
Exit(ERR_TESTS_FAILURES_FOUND);
}
}
catch (Exception)
{
lock (isRunningLock)
{
isRunning = false;
}
Exit(ERR_APPLICATION_OUTPUT_CAPTURE_FAILURE);
}
}
}
}
}