Skip to content

Commit

Permalink
Remove Invoke-CommandInDesktopPackage use (#3658)
Browse files Browse the repository at this point in the history
With the ADO pipeline images being Sever 2022, we don't need this workaround any more.
  • Loading branch information
JohnMcPMS committed Sep 24, 2023
1 parent 5de147c commit db16cfb
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 171 deletions.
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
{
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit db16cfb

Please sign in to comment.