Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Invoke-CommandInDesktopPackage use #3658

Merged
merged 8 commits into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp
Expand Up @@ -160,6 +160,7 @@ namespace AppInstaller::CLI::Workflow
if (SUCCEEDED(hr) || force)
{
auto featureName = dependency.Id();
AICLI_LOG(Core, Verbose, << "Processing Windows Feature dependency [" << featureName << "]");
WindowsFeature::WindowsFeature windowsFeature = dismHelper->GetWindowsFeature(featureName);

if (windowsFeature.DoesExist())
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/Workflows/WorkflowBase.h
Expand Up @@ -65,6 +65,8 @@ namespace AppInstaller::CLI::Workflow
virtual void operator()(Execution::Context& context) const;

const std::string& GetName() const { return m_name; }
bool IsFunction() const { return m_isFunc; }
Func Function() const { return m_func; }

private:
bool m_isFunc = false;
Expand Down
4 changes: 2 additions & 2 deletions src/AppInstallerCLIE2ETests/AppShutdownTests.cs
Expand Up @@ -68,7 +68,7 @@ public void RegisterApplicationTest()
// This just waits for the app termination event.
var testCmdTask = new Task<TestCommon.RunCommandResult>(() =>
{
return TestCommon.RunAICLICommandViaInvokeCommandInDesktopPackage("test", "appshutdown", timeOut: 300000, throwOnTimeout: false);
return TestCommon.RunAICLICommand("test", "appshutdown", timeOut: 300000, throwOnTimeout: false);
});

// Register the app with the updated version.
Expand Down Expand Up @@ -108,7 +108,7 @@ public void RegisterApplicationTest_Force()
throw new NullReferenceException("AICLIPackagePath");
}

var result = TestCommon.RunAICLICommandViaInvokeCommandInDesktopPackage("test", "appshutdown --force", timeOut: 300000, throwOnTimeout: false);
var result = TestCommon.RunAICLICommand("test", "appshutdown --force", timeOut: 300000, throwOnTimeout: false);
TestContext.Out.Write(result.StdOut);
Assert.True(result.StdOut.Contains("Succeeded waiting for app shutdown event"));
}
Expand Down
240 changes: 85 additions & 155 deletions src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
Expand Up @@ -13,8 +13,8 @@ namespace AppInstallerCLIE2ETests.Helpers
using System.Linq;
using System.Management.Automation;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Xml.Linq;
using AppInstallerCLIE2ETests;
using AppInstallerCLIE2ETests.PowerShell;
using Microsoft.Management.Deployment;
Expand Down Expand Up @@ -85,8 +85,9 @@ public enum TestModuleLocation
/// <param name="parameters">Parameters.</param>
/// <param name="stdIn">Optional std in.</param>
/// <param name="timeOut">Optional timeout.</param>
/// <param name="throwOnTimeout">Throw on timeout.</param>
/// <returns>The result of the command.</returns>
public static RunCommandResult RunAICLICommand(string command, string parameters, string stdIn = null, int timeOut = 60000)
public static RunCommandResult RunAICLICommand(string command, string parameters, string stdIn = null, int timeOut = 60000, bool throwOnTimeout = true)
{
string inputMsg =
"AICLI path: " + TestSetup.Parameters.AICLIPath +
Expand All @@ -95,160 +96,9 @@ public static RunCommandResult RunAICLICommand(string command, string parameters
(string.IsNullOrEmpty(stdIn) ? string.Empty : " StdIn: " + stdIn) +
" Timeout: " + timeOut;

TestContext.Out.WriteLine($"Starting command run. {inputMsg} InvokeCommandInDesktopPackage: {TestSetup.Parameters.InvokeCommandInDesktopPackage}");

if (TestSetup.Parameters.InvokeCommandInDesktopPackage)
{
return RunAICLICommandViaInvokeCommandInDesktopPackage(command, parameters, stdIn, timeOut);
}
else
{
return RunAICLICommandViaDirectProcess(command, parameters, stdIn, timeOut);
}
}

/// <summary>
/// Run winget command via direct process.
/// </summary>
/// <param name="command">Command to run.</param>
/// <param name="parameters">Parameters.</param>
/// <param name="stdIn">Optional std in.</param>
/// <param name="timeOut">Optional timeout.</param>
/// <returns>The result of the command.</returns>
public static RunCommandResult RunAICLICommandViaDirectProcess(string command, string parameters, string stdIn = null, int timeOut = 60000)
{
RunCommandResult result = new ();
Process p = new Process();
p.StartInfo = new ProcessStartInfo(TestSetup.Parameters.AICLIPath, command + ' ' + parameters);
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;

if (!string.IsNullOrEmpty(stdIn))
{
p.StartInfo.RedirectStandardInput = true;
}

p.Start();

if (!string.IsNullOrEmpty(stdIn))
{
p.StandardInput.Write(stdIn);
}

if (p.WaitForExit(timeOut))
{
result.ExitCode = p.ExitCode;
result.StdOut = p.StandardOutput.ReadToEnd();
result.StdErr = p.StandardError.ReadToEnd();

TestContext.Out.WriteLine("Command run completed with exit code: " + result.ExitCode);

if (!string.IsNullOrEmpty(result.StdErr))
{
TestContext.Error.WriteLine("Command run error. Error: " + result.StdErr);
}

if (TestSetup.Parameters.VerboseLogging && !string.IsNullOrEmpty(result.StdOut))
{
TestContext.Out.WriteLine("Command run output. Output: " + result.StdOut);
}
}
else
{
throw new TimeoutException($"Direct winget command run timed out: {command} {parameters}");
}

return result;
}

/// <summary>
/// This method is used when the test is run in an OS that does not support AppExecutionAlias. E,g, our build machine.
/// There is not any existing API that'll activate a packaged app and wait for result, and not possible to capture the stdIn and stdOut.
/// This method tries to call Invoke-CommandInDesktopPackage PS command to make test executable run in packaged context.
/// Since Invoke-CommandInDesktopPackage just launches the executable and return, we use cmd pipe to get execution results.
/// The final constructed command will look like:
/// Invoke-CommandInDesktopPackage ...... -Command cmd.exe -Args '-c [cmd command]'
/// where [cmd command] will look like: "echo stdIn | appinst.exe args > stdout.txt 2> stderr.txt &amp;amp; echo %ERRORLEVEL% > exitcode.txt"
/// Then this method will read the piped result and return as RunCommandResult.
/// </summary>
/// <param name="command">Command to run.</param>
/// <param name="parameters">Parameters.</param>
/// <param name="stdIn">Optional std in.</param>
/// <param name="timeOut">Optional timeout.</param>
/// <param name="throwOnTimeout">Throw on timeout.</param>
/// <returns>The result of the command.</returns>
public static RunCommandResult RunAICLICommandViaInvokeCommandInDesktopPackage(string command, string parameters, string stdIn = null, int timeOut = 60000, bool throwOnTimeout = true)
JohnMcPMS marked this conversation as resolved.
Show resolved Hide resolved
{
string cmdCommandPiped = string.Empty;
if (!string.IsNullOrEmpty(stdIn))
{
cmdCommandPiped += $"echo {stdIn} | ";
}

string workDirectory = GetRandomTestDir();
string tempBatchFile = Path.Combine(workDirectory, "Batch.cmd");
string exitCodeFile = Path.Combine(workDirectory, "ExitCode.txt");
string stdOutFile = Path.Combine(workDirectory, "StdOut.txt");
string stdErrFile = Path.Combine(workDirectory, "StdErr.txt");

// First change the codepage so that the rest of the batch file works
cmdCommandPiped += $"chcp 65001\n{TestSetup.Parameters.AICLIPath} {command} {parameters} > {stdOutFile} 2> {stdErrFile}\necho %ERRORLEVEL% > {exitCodeFile}";
File.WriteAllText(tempBatchFile, cmdCommandPiped, new System.Text.UTF8Encoding(false));

string psCommand = $"Invoke-CommandInDesktopPackage -PackageFamilyName {Constants.AICLIPackageFamilyName} -AppId {Constants.AICLIAppId} -PreventBreakaway -Command cmd.exe -Args '/c \"{tempBatchFile}\"'";

var psInvokeResult = RunCommandWithResult("powershell", psCommand);

if (psInvokeResult.ExitCode != 0)
{
// PS invocation failed, return result and no need to check piped output.
return psInvokeResult;
}

// The PS command just launches the app and immediately returns, we'll have to wait for up to the timeOut specified here
int waitedTime = 0;
while (!File.Exists(exitCodeFile) && waitedTime <= timeOut)
{
Thread.Sleep(1000);
waitedTime += 1000;
}

if (waitedTime >= timeOut && throwOnTimeout)
{
throw new TimeoutException($"Packaged winget command run timed out: {command} {parameters}");
}

RunCommandResult result = new ();

// Sometimes the files are still in use; allow for this with a wait and retry loop.
for (int retryCount = 0; retryCount < 4; ++retryCount)
{
bool success = false;

try
{
result.ExitCode = File.Exists(exitCodeFile) ? int.Parse(File.ReadAllText(exitCodeFile).Trim()) : unchecked((int)0x80004005);
result.StdOut = File.Exists(stdOutFile) ? File.ReadAllText(stdOutFile) : string.Empty;
result.StdErr = File.Exists(stdErrFile) ? File.ReadAllText(stdErrFile) : string.Empty;
success = true;
}
catch (Exception e)
{
TestContext.Out.WriteLine("Failed to access files: " + e.Message);
}

if (success)
{
break;
}
else
{
Thread.Sleep(250);
}
}
TestContext.Out.WriteLine($"Starting command run. {inputMsg}");

return result;
return RunAICLICommandViaDirectProcess(command, parameters, stdIn, timeOut, throwOnTimeout);
}

/// <summary>
Expand Down Expand Up @@ -1027,6 +877,86 @@ public static string GetExpectedModulePath(TestModuleLocation location)
}
}

/// <summary>
/// Run winget command via direct process.
/// </summary>
/// <param name="command">Command to run.</param>
/// <param name="parameters">Parameters.</param>
/// <param name="stdIn">Optional std in.</param>
/// <param name="timeOut">Optional timeout.</param>
/// <param name="throwOnTimeout">Throw on timeout.</param>
/// <returns>The result of the command.</returns>
private static RunCommandResult RunAICLICommandViaDirectProcess(string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout)
{
RunCommandResult result = new ();
Process p = new Process();
p.StartInfo = new ProcessStartInfo(TestSetup.Parameters.AICLIPath, command + ' ' + parameters);
p.StartInfo.UseShellExecute = false;

p.StartInfo.RedirectStandardOutput = true;
StringBuilder outputData = new ();
p.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
outputData.AppendLine(args.Data);
}
};

p.StartInfo.RedirectStandardError = true;
StringBuilder errorData = new ();
p.ErrorDataReceived += (sender, args) =>
{
if (args.Data != null)
{
errorData.AppendLine(args.Data);
}
};

if (!string.IsNullOrEmpty(stdIn))
{
p.StartInfo.RedirectStandardInput = true;
}

p.Start();
p.BeginOutputReadLine();
p.BeginErrorReadLine();

if (!string.IsNullOrEmpty(stdIn))
{
p.StandardInput.Write(stdIn);
}

if (p.WaitForExit(timeOut))
{
// According to documentation, this extra call will ensure that the redirected streams
// have finished reading all of the data.
p.WaitForExit();

result.ExitCode = p.ExitCode;
result.StdOut = outputData.ToString();
result.StdErr = errorData.ToString();

TestContext.Out.WriteLine("Command run completed with exit code: " + result.ExitCode);

if (!string.IsNullOrEmpty(result.StdErr))
{
TestContext.Error.WriteLine("Command run error. Error: " + result.StdErr);
}

if (TestSetup.Parameters.VerboseLogging && !string.IsNullOrEmpty(result.StdOut))
{
TestContext.Out.WriteLine("Command run output. Output: " + result.StdOut);
}
}
else if (throwOnTimeout)
{
throw new TimeoutException($"Direct winget command run timed out: {command} {parameters}");
}

return result;
}

/// <summary>
/// Run command result.
/// </summary>
Expand Down
11 changes: 0 additions & 11 deletions src/AppInstallerCLIE2ETests/Helpers/TestSetup.cs
Expand Up @@ -30,18 +30,12 @@ private TestSetup()
this.PackagedContext = this.InitializeBoolParam(Constants.PackagedContextParameter, true);
this.VerboseLogging = this.InitializeBoolParam(Constants.VerboseLoggingParameter, true);
this.LooseFileRegistration = this.InitializeBoolParam(Constants.LooseFileRegistrationParameter);
this.InvokeCommandInDesktopPackage = this.InitializeBoolParam(Constants.InvokeCommandInDesktopPackageParameter);
this.SkipTestSource = this.InitializeBoolParam(Constants.SkipTestSourceParameter, this.IsDefault);

// For packaged context, default to AppExecutionAlias
this.AICLIPath = this.InitializeStringParam(Constants.AICLIPathParameter, this.PackagedContext ? "WinGetDev.exe" : TestCommon.GetTestFile("winget.exe"));
this.AICLIPackagePath = this.InitializeStringParam(Constants.AICLIPackagePathParameter, TestCommon.GetTestFile("AppInstallerCLIPackage.appxbundle"));

if (this.LooseFileRegistration && this.InvokeCommandInDesktopPackage)
{
this.AICLIPath = Path.Combine(this.AICLIPackagePath, this.AICLIPath);
}

this.StaticFileRootPath = this.InitializeDirectoryParam(Constants.StaticFileRootPathParameter, Path.GetTempPath());

this.PowerShellModuleManifestPath = this.InitializeFileParam(Constants.PowerShellModulePathParameter);
Expand Down Expand Up @@ -88,11 +82,6 @@ public static TestSetup Parameters
/// </summary>
public bool LooseFileRegistration { get; }

/// <summary>
/// Gets a value indicating whether to invoke command in desktop package.
/// </summary>
public bool InvokeCommandInDesktopPackage { get; }

/// <summary>
/// Gets the static file root path.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLIE2ETests/InstallCommand.cs
Expand Up @@ -629,6 +629,7 @@ public void InstallExeWithLatestInstalledWithForce()
/// Test install a package with an invalid Windows Feature dependency.
/// </summary>
[Test]
[Ignore("Need change to implementation of Windows Feature dependencies.")]
public void InstallWithWindowsFeatureDependency_FeatureNotFound()
{
var testDir = TestCommon.GetRandomTestDir();
Expand All @@ -641,6 +642,7 @@ public void InstallWithWindowsFeatureDependency_FeatureNotFound()
/// Test install a package with a Windows Feature dependency using the force argument.
/// </summary>
[Test]
[Ignore("Need change to implementation of Windows Feature dependencies.")]
public void InstallWithWindowsFeatureDependency_Force()
{
var testDir = TestCommon.GetRandomTestDir();
Expand Down
4 changes: 1 addition & 3 deletions templates/e2e-test.template.yml
Expand Up @@ -23,17 +23,15 @@ steps:
${{ if eq(parameters.isPackaged, true) }}:
overrideTestrunParameters: '-PackagedContext true
-AICLIPackagePath $(packageLayoutDir)
-AICLIPath AppInstallerCLI\winget.exe
-AICLIPath wingetdev.exe
-LooseFileRegistration true
-InvokeCommandInDesktopPackage true
-StaticFileRootPath $(Agent.TempDirectory)\TestLocalIndex
-PowerShellModulePath $(buildOutDir)\PowerShell\Microsoft.WinGet.Client\Microsoft.WinGet.Client.psd1
-LocalServerCertPath $(Agent.TempDirectory)\servercert.cer
-SkipTestSource true'
${{ else }}:
overrideTestrunParameters: '-PackagedContext false
-AICLIPath $(packageLayoutDir)\AppInstallerCLI\winget.exe
-InvokeCommandInDesktopPackage false
-StaticFileRootPath $(Agent.TempDirectory)\TestLocalIndex
-PowerShellModulePath $(buildOutDir)\PowerShell\Microsoft.WinGet.Client\Microsoft.WinGet.Client.psd1
-LocalServerCertPath $(Agent.TempDirectory)\servercert.cer
Expand Down