607 lines
24 KiB
C#
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);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|