diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/TestWtIntuneWin.cs b/src/Svrooij.WinTuner.CmdLets/Commands/TestWtIntuneWin.cs
new file mode 100644
index 0000000..0f43329
--- /dev/null
+++ b/src/Svrooij.WinTuner.CmdLets/Commands/TestWtIntuneWin.cs
@@ -0,0 +1,190 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Graph.Beta;
+using Svrooij.PowerShell.DependencyInjection;
+using System;
+using System.IO;
+using System.Linq;
+using System.Management.Automation;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using WingetIntune;
+using WingetIntune.Graph;
+using WingetIntune.Intune;
+using WingetIntune.Models;
+using WingetIntune.Testing;
+using GraphModels = Microsoft.Graph.Beta.Models;
+
+namespace Svrooij.WinTuner.CmdLets.Commands;
+///
+/// Test if a package will install
+/// Test if a package will install on the Windows Sandbox
+/// Documentation
+///
+///
+/// Test a packaged installer in sandbox
+/// Test-WtIntuneWin -PackageFolder D:\packages\JanDeDobbeleer.OhMyPosh\22.0.3
+///
+[Cmdlet(VerbsDiagnostic.Test, "WtIntuneWin", DefaultParameterSetName = nameof(PackageFolder))]
+[OutputType(typeof(string))]
+public class TestWtIntuneWin : DependencyCmdlet
+{
+ private const string ParameterSetWinGet = "WinGet";
+ private const string ParameterSetIntuneWin = "IntuneWin";
+
+ ///
+ /// The package id to upload to Intune.
+ ///
+ [Parameter(
+ Mandatory = true,
+ Position = 0,
+ ParameterSetName = ParameterSetWinGet,
+ ValueFromPipeline = false,
+ ValueFromPipelineByPropertyName = false,
+ HelpMessage = "The package id to upload to Intune.")]
+ public string? PackageId { get; set; }
+
+ ///
+ /// The version to upload to Intune
+ ///
+ [Parameter(
+ Mandatory = true,
+ Position = 1,
+ ParameterSetName = ParameterSetWinGet,
+ ValueFromPipeline = false,
+ HelpMessage = "The version to upload to Intune"
+ )]
+ public string? Version { get; set; }
+
+ ///
+ /// The Root folder where all the package live in.
+ ///
+ [Parameter(
+ Mandatory = true,
+ Position = 2,
+ ParameterSetName = ParameterSetWinGet,
+ ValueFromPipeline = false,
+ HelpMessage = "The Root folder where all the package live in.")]
+ public string? RootPackageFolder { get; set; }
+
+ ///
+ /// The folder where the package is
+ ///
+ [Parameter(
+ Mandatory = true,
+ Position = 0,
+ ParameterSetName = nameof(PackageFolder),
+ ValueFromPipeline = false,
+ ValueFromPipelineByPropertyName = true,
+ HelpMessage = "The folder where the package is")]
+ public string? PackageFolder { get; set; }
+
+ ///
+ /// The IntuneWin file to test
+ ///
+ [Parameter(
+ Mandatory = true,
+ Position = 0,
+ ParameterSetName = ParameterSetIntuneWin,
+ ValueFromPipeline = false,
+ ValueFromPipelineByPropertyName = true,
+ HelpMessage = "The IntuneWin file to test")]
+ public string? IntuneWinFile { get; set; }
+
+ ///
+ /// The installer filename (if not set correctly inside the intunewin)
+ ///
+ [Parameter(
+ Mandatory = false,
+ Position = 1,
+ ParameterSetName = ParameterSetIntuneWin,
+ ValueFromPipeline = false,
+ ValueFromPipelineByPropertyName = true,
+ HelpMessage = "The installer filename (if not set correctly inside the intunewin)")]
+ public string? InstallerFilename { get; set; }
+
+ ///
+ /// The installer arguments (if you want it to execute silently)
+ ///
+ [Parameter(
+ Mandatory = false,
+ Position = 2,
+ ParameterSetName = ParameterSetIntuneWin,
+ ValueFromPipeline = false,
+ ValueFromPipelineByPropertyName = true,
+ HelpMessage = "The installer arguments (if you want it to execute silently)")]
+ [Parameter(
+ Mandatory = false,
+ Position = 2,
+ ParameterSetName = nameof(PackageFolder),
+ ValueFromPipeline = false,
+ ValueFromPipelineByPropertyName = false,
+ DontShow = true,
+ HelpMessage = "Override the installer arguments")]
+ public string? InstallerArguments { get; set; }
+
+ ///
+ /// Clean the test files after run
+ ///
+ [Parameter(
+ Mandatory = false,
+ HelpMessage = "Clean the test files after run")]
+ public SwitchParameter Clean { get; set; }
+
+ ///
+ /// Sleep for x seconds before closing
+ ///
+ [Parameter(
+ Mandatory = false,
+ HelpMessage = "Sleep for x seconds before auto shutdown")]
+ public int? Sleep { get; set; }
+
+ [ServiceDependency]
+ private ILogger? logger;
+
+ [ServiceDependency]
+ private WindowsSandbox? sandbox;
+
+ [ServiceDependency]
+ private MetadataManager? metadataManager;
+
+ ///
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (ParameterSetName == ParameterSetWinGet)
+ {
+ logger?.LogDebug("Loading package details from RootPackageFolder {RootPackageFolder}, PackageId {PackageId}, Version {Version}", RootPackageFolder, PackageId, Version);
+ PackageFolder = Path.Combine(RootPackageFolder!, PackageId!, Version!);
+ logger?.LogDebug("Loading package details from folder {packageFolder}", PackageFolder);
+ }
+
+ if (PackageFolder is not null)
+ {
+ logger?.LogInformation("Loading package details from folder {packageFolder}", PackageFolder);
+ var packageInfo = await metadataManager!.LoadPackageInfoFromFolderAsync(PackageFolder, cancellationToken);
+ InstallerFilename = packageInfo.InstallerFilename;
+ // If the installer arguments are not set, use the ones from the package info.
+ InstallerArguments ??= packageInfo.InstallCommandLine?.Replace($"\"{packageInfo.InstallerFilename!}\" ", "");
+ IntuneWinFile = metadataManager.GetIntuneWinFileName(PackageFolder, packageInfo);
+ }
+
+ if (IntuneWinFile is null)
+ {
+ var ex = new ArgumentException("PackageFolder was provided");
+ logger?.LogError(ex, "PackageFolder was provided");
+ throw ex;
+ }
+
+ var sandboxFile = await sandbox!.PrepareSandboxFileForPackage(IntuneWinFile!, InstallerFilename, InstallerArguments, timeout: Sleep, cancellationToken: cancellationToken);
+ logger?.LogDebug("Sandbox file created at {sandboxFile}", sandboxFile);
+ var result = await sandbox.RunSandbox(sandboxFile, Clean, cancellationToken);
+ if (result is null)
+ {
+ logger?.LogError("Sandbox exited with null result");
+ return;
+ }
+
+ logger?.LogInformation("Installed {InstallerFilename} in sandbox, reported exitcode {ExitCode}, number of apps installed {AppsInstalled}", InstallerFilename, result.ExitCode, result?.InstalledApps?.Count());
+ logger?.LogInformation("Sandbox result: {Result}", result);
+ }
+}
diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/TestWtSetupFile.cs b/src/Svrooij.WinTuner.CmdLets/Commands/TestWtSetupFile.cs
new file mode 100644
index 0000000..ed71d85
--- /dev/null
+++ b/src/Svrooij.WinTuner.CmdLets/Commands/TestWtSetupFile.cs
@@ -0,0 +1,88 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Graph.Beta;
+using Svrooij.PowerShell.DependencyInjection;
+using System;
+using System.IO;
+using System.Linq;
+using System.Management.Automation;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using WingetIntune;
+using WingetIntune.Graph;
+using WingetIntune.Intune;
+using WingetIntune.Models;
+using WingetIntune.Testing;
+using GraphModels = Microsoft.Graph.Beta.Models;
+
+namespace Svrooij.WinTuner.CmdLets.Commands;
+///
+/// Test your silent install switches
+/// Test if a setup will install on the Windows Sandbox
+/// Documentation
+///
+///
+/// Test any installer in sandbox
+/// Test-WtSetupFile -SetupFile D:\packages\xyz.exe -Installer "all your arguments"
+///
+[Cmdlet(VerbsDiagnostic.Test, "WtSetupFile")]
+[OutputType(typeof(string))]
+public class TestWtSetupFile : DependencyCmdlet
+{
+ ///
+ /// The absolute path to your setup file
+ ///
+ [Parameter(
+ Mandatory = true,
+ Position = 0,
+ ValueFromPipeline = false,
+ ValueFromPipelineByPropertyName = true,
+ HelpMessage = "Absolute path to your setup file")]
+ public string? SetupFile { get; set; }
+
+ ///
+ /// Override the installer arguments
+ ///
+ [Parameter(
+ Mandatory = false,
+ Position = 1,
+ ValueFromPipeline = false,
+ ValueFromPipelineByPropertyName = false,
+ HelpMessage = "Override the installer arguments")]
+ public string? InstallerArguments { get; set; }
+
+ ///
+ /// Sleep for x seconds before closing
+ ///
+ [Parameter(
+ Mandatory = false,
+ HelpMessage = "Sleep for x seconds before auto shutdown")]
+ public int? Sleep { get; set; }
+
+ [ServiceDependency]
+ private ILogger? logger;
+
+ [ServiceDependency]
+ private WindowsSandbox? sandbox;
+
+ [ServiceDependency]
+ private MetadataManager? metadataManager;
+
+ ///
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+
+
+ var sandboxFile = await sandbox!.PrepareSandboxForInstaller(SetupFile!, InstallerArguments, Sleep, cancellationToken);
+ logger?.LogDebug("Sandbox file created at {sandboxFile}", sandboxFile);
+ var result = await sandbox.RunSandbox(sandboxFile, true, cancellationToken);
+ if (result is null)
+ {
+ logger?.LogError("Sandbox exited with null result");
+ return;
+ }
+
+ logger?.LogInformation("Installed {InstallerFilename} in sandbox, reported exitcode {ExitCode}, number of apps installed {AppsInstalled}", Path.GetFileName(SetupFile), result.ExitCode, result?.InstalledApps?.Count());
+ logger?.LogInformation("Sandbox result: {Result}", result);
+ }
+}
diff --git a/src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.dll-Help.xml b/src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.dll-Help.xml
index a3e2e15..04d3033 100644
--- a/src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.dll-Help.xml
+++ b/src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.dll-Help.xml
@@ -1928,6 +1928,18 @@
None
+
+ Locale
+
+ The desired locale, if available (eg. 'en-US')
+
+ String
+
+ String
+
+
+ None
+
@@ -2027,6 +2039,18 @@
None
+
+ Locale
+
+ The desired locale, if available (eg. 'en-US')
+
+ String
+
+ String
+
+
+ None
+
@@ -2501,6 +2525,549 @@
+
+
+ Test-WtIntuneWin
+ Test
+ WtIntuneWin
+
+ Test if a package will install
+
+
+
+ Test if a package will install on the Windows Sandbox
+
+
+
+ Test-WtIntuneWin
+
+ PackageFolder
+
+ The folder where the package is
+
+ String
+
+ String
+
+
+ None
+
+
+ InstallerArguments
+
+ The installer arguments (if you want it to execute silently)
+
+ String
+
+ String
+
+
+ None
+
+
+ Clean
+
+ Clean the test files after run
+
+
+ SwitchParameter
+
+
+ False
+
+
+ Sleep
+
+ Sleep for x seconds before auto shutdown
+
+ Int32
+
+ Int32
+
+
+ None
+
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+ Test-WtIntuneWin
+
+ IntuneWinFile
+
+ The IntuneWin file to test
+
+ String
+
+ String
+
+
+ None
+
+
+ InstallerFilename
+
+ The installer filename (if not set correctly inside the intunewin)
+
+ String
+
+ String
+
+
+ None
+
+
+ InstallerArguments
+
+ The installer arguments (if you want it to execute silently)
+
+ String
+
+ String
+
+
+ None
+
+
+ Clean
+
+ Clean the test files after run
+
+
+ SwitchParameter
+
+
+ False
+
+
+ Sleep
+
+ Sleep for x seconds before auto shutdown
+
+ Int32
+
+ Int32
+
+
+ None
+
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+ Test-WtIntuneWin
+
+ PackageId
+
+ The package id to upload to Intune.
+
+ String
+
+ String
+
+
+ None
+
+
+ Version
+
+ The version to upload to Intune
+
+ String
+
+ String
+
+
+ None
+
+
+ RootPackageFolder
+
+ The Root folder where all the package live in.
+
+ String
+
+ String
+
+
+ None
+
+
+ Clean
+
+ Clean the test files after run
+
+
+ SwitchParameter
+
+
+ False
+
+
+ Sleep
+
+ Sleep for x seconds before auto shutdown
+
+ Int32
+
+ Int32
+
+
+ None
+
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+
+
+ Clean
+
+ Clean the test files after run
+
+ SwitchParameter
+
+ SwitchParameter
+
+
+ False
+
+
+ InstallerArguments
+
+ The installer arguments (if you want it to execute silently)
+
+ String
+
+ String
+
+
+ None
+
+
+ InstallerFilename
+
+ The installer filename (if not set correctly inside the intunewin)
+
+ String
+
+ String
+
+
+ None
+
+
+ IntuneWinFile
+
+ The IntuneWin file to test
+
+ String
+
+ String
+
+
+ None
+
+
+ PackageFolder
+
+ The folder where the package is
+
+ String
+
+ String
+
+
+ None
+
+
+ PackageId
+
+ The package id to upload to Intune.
+
+ String
+
+ String
+
+
+ None
+
+
+ RootPackageFolder
+
+ The Root folder where all the package live in.
+
+ String
+
+ String
+
+
+ None
+
+
+ Sleep
+
+ Sleep for x seconds before auto shutdown
+
+ Int32
+
+ Int32
+
+
+ None
+
+
+ Version
+
+ The version to upload to Intune
+
+ String
+
+ String
+
+
+ None
+
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+
+
+ System.String
+
+
+
+
+
+
+
+
+
+ System.String
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -------------------------- Example 1 --------------------------
+ PS C:\> Test-WtIntuneWin -PackageFolder D:\packages\JanDeDobbeleer.OhMyPosh\22.0.3
+
+ Test a packaged installer in sandbox
+
+
+
+
+
+ Online Version:
+ https://wintuner.app/docs/wintuner-powershell/Test-WtIntuneWin
+
+
+
+
+
+ Test-WtSetupFile
+ Test
+ WtSetupFile
+
+ Test your silent install switches
+
+
+
+ Test if a setup will install on the Windows Sandbox
+
+
+
+ Test-WtSetupFile
+
+ SetupFile
+
+ Absolute path to your setup file
+
+ String
+
+ String
+
+
+ None
+
+
+ InstallerArguments
+
+ Override the installer arguments
+
+ String
+
+ String
+
+
+ None
+
+
+ Sleep
+
+ Sleep for x seconds before auto shutdown
+
+ Int32
+
+ Int32
+
+
+ None
+
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+
+
+ InstallerArguments
+
+ Override the installer arguments
+
+ String
+
+ String
+
+
+ None
+
+
+ SetupFile
+
+ Absolute path to your setup file
+
+ String
+
+ String
+
+
+ None
+
+
+ Sleep
+
+ Sleep for x seconds before auto shutdown
+
+ Int32
+
+ Int32
+
+
+ None
+
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+
+
+ System.String
+
+
+
+
+
+
+
+
+
+ System.String
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -------------------------- Example 1 --------------------------
+ PS C:\> Test-WtSetupFile -SetupFile D:\packages\xyz.exe -Installer "all your arguments"
+
+ Test any installer in sandbox
+
+
+
+
+
+ Online Version:
+ https://wintuner.app/docs/wintuner-powershell/Test-WtSetupFile
+
+
+
Unprotect-IntuneWinPackage
diff --git a/src/Svrooij.WinTuner.CmdLets/docs/New-WtWingetPackage.md b/src/Svrooij.WinTuner.CmdLets/docs/New-WtWingetPackage.md
index 60ca28f..2824d26 100644
--- a/src/Svrooij.WinTuner.CmdLets/docs/New-WtWingetPackage.md
+++ b/src/Svrooij.WinTuner.CmdLets/docs/New-WtWingetPackage.md
@@ -15,7 +15,7 @@ Create intunewin file from Winget installer
```
New-WtWingetPackage [-PackageId] [[-PackageFolder] ] [[-Version] ]
[[-TempFolder] ] [-Architecture ] [-InstallerContext ]
- [-PackageScript ] [-ProgressAction ] []
+ [-PackageScript ] [-Locale ] [-ProgressAction ] []
```
## DESCRIPTION
@@ -152,6 +152,21 @@ Accept pipeline input: True (ByPropertyName, ByValue)
Accept wildcard characters: False
```
+### -Locale
+The desired locale, if available (eg. 'en-US')
+
+```yaml
+Type: String
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: True (ByPropertyName, ByValue)
+Accept wildcard characters: False
+```
+
### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
diff --git a/src/Svrooij.WinTuner.CmdLets/docs/Svrooij.WinTuner.CmdLets.md b/src/Svrooij.WinTuner.CmdLets/docs/Svrooij.WinTuner.CmdLets.md
index a3992a3..4ab6b7e 100644
--- a/src/Svrooij.WinTuner.CmdLets/docs/Svrooij.WinTuner.CmdLets.md
+++ b/src/Svrooij.WinTuner.CmdLets/docs/Svrooij.WinTuner.CmdLets.md
@@ -32,6 +32,12 @@ Remove an app from Intune
### [Search-WtWinGetPackage](Search-WtWinGetPackage.md)
Search for packages in winget
+### [Test-WtIntuneWin](Test-WtIntuneWin.md)
+Test if a package will install
+
+### [Test-WtSetupFile](Test-WtSetupFile.md)
+Test your silent install switches
+
### [Unprotect-IntuneWinPackage](Unprotect-IntuneWinPackage.md)
Decrypt an IntuneWin package
diff --git a/src/Svrooij.WinTuner.CmdLets/docs/Test-WtIntuneWin.md b/src/Svrooij.WinTuner.CmdLets/docs/Test-WtIntuneWin.md
new file mode 100644
index 0000000..07d9a71
--- /dev/null
+++ b/src/Svrooij.WinTuner.CmdLets/docs/Test-WtIntuneWin.md
@@ -0,0 +1,222 @@
+---
+external help file: Svrooij.WinTuner.CmdLets.dll-Help.xml
+Module Name: Svrooij.WinTuner.CmdLets
+online version: https://wintuner.app/docs/wintuner-powershell/Test-WtIntuneWin
+schema: 2.0.0
+---
+
+# Test-WtIntuneWin
+
+## SYNOPSIS
+Test if a package will install
+
+## SYNTAX
+
+### PackageFolder (Default)
+```
+Test-WtIntuneWin [-PackageFolder] [[-InstallerArguments] ] [-Clean] [-Sleep ]
+ [-ProgressAction ] []
+```
+
+### WinGet
+```
+Test-WtIntuneWin [-PackageId] [-Version] [-RootPackageFolder] [-Clean]
+ [-Sleep ] [-ProgressAction ] []
+```
+
+### IntuneWin
+```
+Test-WtIntuneWin [-IntuneWinFile] [[-InstallerFilename] ] [[-InstallerArguments] ]
+ [-Clean] [-Sleep ] [-ProgressAction ] []
+```
+
+## DESCRIPTION
+Test if a package will install on the Windows Sandbox
+
+## EXAMPLES
+
+### Example 1
+```powershell
+PS C:\> Test-WtIntuneWin -PackageFolder D:\packages\JanDeDobbeleer.OhMyPosh\22.0.3
+```
+
+Test a packaged installer in sandbox
+
+## PARAMETERS
+
+### -Clean
+Clean the test files after run
+
+```yaml
+Type: SwitchParameter
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -InstallerArguments
+The installer arguments (if you want it to execute silently)
+
+```yaml
+Type: String
+Parameter Sets: PackageFolder
+Aliases:
+
+Required: False
+Position: 2
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+```yaml
+Type: String
+Parameter Sets: IntuneWin
+Aliases:
+
+Required: False
+Position: 2
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -InstallerFilename
+The installer filename (if not set correctly inside the intunewin)
+
+```yaml
+Type: String
+Parameter Sets: IntuneWin
+Aliases:
+
+Required: False
+Position: 1
+Default value: None
+Accept pipeline input: True (ByPropertyName)
+Accept wildcard characters: False
+```
+
+### -IntuneWinFile
+The IntuneWin file to test
+
+```yaml
+Type: String
+Parameter Sets: IntuneWin
+Aliases:
+
+Required: True
+Position: 0
+Default value: None
+Accept pipeline input: True (ByPropertyName)
+Accept wildcard characters: False
+```
+
+### -PackageFolder
+The folder where the package is
+
+```yaml
+Type: String
+Parameter Sets: PackageFolder
+Aliases:
+
+Required: True
+Position: 0
+Default value: None
+Accept pipeline input: True (ByPropertyName)
+Accept wildcard characters: False
+```
+
+### -PackageId
+The package id to upload to Intune.
+
+```yaml
+Type: String
+Parameter Sets: WinGet
+Aliases:
+
+Required: True
+Position: 0
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -RootPackageFolder
+The Root folder where all the package live in.
+
+```yaml
+Type: String
+Parameter Sets: WinGet
+Aliases:
+
+Required: True
+Position: 2
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -Sleep
+Sleep for x seconds before auto shutdown
+
+```yaml
+Type: Int32
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -Version
+The version to upload to Intune
+
+```yaml
+Type: String
+Parameter Sets: WinGet
+Aliases:
+
+Required: True
+Position: 1
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -ProgressAction
+{{ Fill ProgressAction Description }}
+
+```yaml
+Type: ActionPreference
+Parameter Sets: (All)
+Aliases: proga
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### CommonParameters
+This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
+
+## INPUTS
+
+### System.String
+
+## OUTPUTS
+
+### System.String
+
+## NOTES
+
+## RELATED LINKS
diff --git a/src/Svrooij.WinTuner.CmdLets/docs/Test-WtSetupFile.md b/src/Svrooij.WinTuner.CmdLets/docs/Test-WtSetupFile.md
new file mode 100644
index 0000000..acdf3c8
--- /dev/null
+++ b/src/Svrooij.WinTuner.CmdLets/docs/Test-WtSetupFile.md
@@ -0,0 +1,107 @@
+---
+external help file: Svrooij.WinTuner.CmdLets.dll-Help.xml
+Module Name: Svrooij.WinTuner.CmdLets
+online version: https://wintuner.app/docs/wintuner-powershell/Test-WtSetupFile
+schema: 2.0.0
+---
+
+# Test-WtSetupFile
+
+## SYNOPSIS
+Test your silent install switches
+
+## SYNTAX
+
+```
+Test-WtSetupFile [-SetupFile] [[-InstallerArguments] ] [-Sleep ]
+ [-ProgressAction ] []
+```
+
+## DESCRIPTION
+Test if a setup will install on the Windows Sandbox
+
+## EXAMPLES
+
+### Example 1
+```powershell
+PS C:\> Test-WtSetupFile -SetupFile D:\packages\xyz.exe -Installer "all your arguments"
+```
+
+Test any installer in sandbox
+
+## PARAMETERS
+
+### -InstallerArguments
+Override the installer arguments
+
+```yaml
+Type: String
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: 1
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -SetupFile
+Absolute path to your setup file
+
+```yaml
+Type: String
+Parameter Sets: (All)
+Aliases:
+
+Required: True
+Position: 0
+Default value: None
+Accept pipeline input: True (ByPropertyName)
+Accept wildcard characters: False
+```
+
+### -Sleep
+Sleep for x seconds before auto shutdown
+
+```yaml
+Type: Int32
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -ProgressAction
+{{ Fill ProgressAction Description }}
+
+```yaml
+Type: ActionPreference
+Parameter Sets: (All)
+Aliases: proga
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### CommonParameters
+This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
+
+## INPUTS
+
+### System.String
+
+## OUTPUTS
+
+### System.String
+
+## NOTES
+
+## RELATED LINKS
diff --git a/src/WingetIntune/Intune/MetadataManager.cs b/src/WingetIntune/Intune/MetadataManager.cs
index c4ba390..10c95b6 100644
--- a/src/WingetIntune/Intune/MetadataManager.cs
+++ b/src/WingetIntune/Intune/MetadataManager.cs
@@ -10,18 +10,32 @@
using WingetIntune.Models;
namespace WingetIntune.Intune;
+///
+/// Makes it easier to work with wintuner metadata files.
+///
public class MetadataManager
{
private readonly ILogger logger;
private readonly IFileManager fileManager;
private readonly Mapper mapper = new();
+ ///
+ ///
+ ///
+ ///
+ ///
public MetadataManager(ILogger logger, IFileManager fileManager)
{
this.logger = logger;
this.fileManager = fileManager;
}
+ ///
+ /// Loads the package info from a folder.
+ ///
+ /// Folder where WinTuner placed a file
+ ///
+ ///
public async Task LoadPackageInfoFromFolderAsync(string folder, CancellationToken cancellationToken)
{
logger.LogDebug("Loading package info from {folder}", folder);
@@ -49,9 +63,22 @@ public async Task LoadPackageInfoFromFolderAsync(string folder, Can
return result;
}
+ ///
+ /// Loads the package info from a folder, with packageId and version
+ ///
+ /// The Root package filer
+ /// Package ID of previously packaged app
+ /// Version of the app
+ ///
+ /// Combines // to a path to get the metadata from
public Task LoadPackageInfoFromFolderAsync(string rootFolder, string packageId, string version, CancellationToken cancellationToken) =>
LoadPackageInfoFromFolderAsync(Path.Combine(rootFolder, packageId, version), cancellationToken);
+ ///
+ /// Converts a package info to a Win32App to upload to Graph
+ ///
+ ///
+ ///
public Win32LobApp ConvertPackageInfoToWin32App(PackageInfo packageInfo)
{
logger.LogDebug("Converting package info to Win32App");
@@ -59,6 +86,12 @@ public Win32LobApp ConvertPackageInfoToWin32App(PackageInfo packageInfo)
return win32App;
}
+ ///
+ /// Gets the IntuneWin file name from a package folder
+ ///
+ ///
+ ///
+ ///
public string GetIntuneWinFileName(string packageFolder, PackageInfo packageInfo)
{
return Path.Combine(packageFolder, Path.GetFileNameWithoutExtension(packageInfo.InstallerFilename!) + ".intunewin");
diff --git a/src/WingetIntune/Testing/WindowsSandbox.cs b/src/WingetIntune/Testing/WindowsSandbox.cs
new file mode 100644
index 0000000..943dc33
--- /dev/null
+++ b/src/WingetIntune/Testing/WindowsSandbox.cs
@@ -0,0 +1,325 @@
+using Microsoft.Extensions.Logging;
+using SvRooij.ContentPrep;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WingetIntune.Models;
+
+namespace WingetIntune.Testing;
+
+///
+/// Helper to test packages in the Windows Sandbox
+///
+public class WindowsSandbox
+{
+ private readonly ILogger logger;
+ private readonly Packager packager;
+ private readonly IProcessManager processManager;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public WindowsSandbox(ILoggerFactory loggerFactory, IProcessManager processManager)
+ {
+ this.logger = loggerFactory.CreateLogger();
+ this.packager = new Packager(loggerFactory.CreateLogger());
+ this.processManager = processManager;
+ }
+
+ ///
+ /// Prepares a sandbox file for a package
+ ///
+ /// The absolute path to the .intunewin file
+ /// Name of the setup file inside the intune win, if not provided the name from the intunewin metadata will be used
+ /// Silent arguments for this setup
+ /// When a number above -1 is provided a shutdown command will be added to the script
+ ///
+ /// The location of the sandbox file, which may be started with method.
+ /// Will decrypt the intunewin file to a temp folder, create install scripts and creates a Windows Sandbox file.
+ public async Task PrepareSandboxFileForPackage(string intuneWinFile, string? installerFilename, string? installerArguments, int? timeout = null, CancellationToken cancellationToken = default)
+ {
+ var outputFolder = Path.Combine(Path.GetTempPath(), "wintuner-sandbox", Guid.NewGuid().ToString());
+ logger.LogInformation("Preparing sandbox file for {IntuneWinFile}", intuneWinFile);
+ //logger.LogInformation("Preparing sandbox file for {PackageId} {Version}", packageInfo.PackageIdentifier, packageInfo.Version);
+
+ var installerFolder = Path.Combine(outputFolder, "installer");
+ var logsFolder = Path.Combine(outputFolder, "logs");
+
+ Directory.CreateDirectory(installerFolder);
+ Directory.CreateDirectory(logsFolder);
+ var info = await packager.Unpack(intuneWinFile, installerFolder, cancellationToken);
+ logger.LogDebug("Unpackaged intunewin file {intuneWinFile} to {installerFolder} contained installer {InstallerFilename}", intuneWinFile, installerFolder, info?.SetupFile);
+ installerFilename ??= info!.SetupFile!;
+ if (!File.Exists(Path.Combine(installerFolder, installerFilename)))
+ {
+ throw new FileNotFoundException("Installer in the unpacked folder", installerFilename!);
+ }
+
+ var sandboxFile = Path.Combine(outputFolder, "sandbox.wsb");
+ await WriteSandboxConfig(sandboxFile, installerFolder, logsFolder);
+ var scriptFolder = Path.Combine(installerFolder, "wt_scripts");
+ await WriteTestScript(scriptFolder, installerFilename, installerArguments, timeout);
+
+ return sandboxFile;
+ }
+
+ ///
+ /// Prepares a sandbox file for an installer
+ ///
+ /// Absolute path to the installer
+ /// Arguments that should be used
+ /// Want to auto shutdown the Sandbox?
+ ///
+ ///
+ ///
+ public async Task PrepareSandboxForInstaller(string setupFile, string? installerArguments, int? timeout = null, CancellationToken cancellationToken = default)
+ {
+ var outputFolder = Path.Combine(Path.GetTempPath(), "wintuner-sandbox", Guid.NewGuid().ToString());
+ var installerFolder = Directory.GetParent(setupFile)!.FullName;
+ logger.LogInformation("Preparing sandbox file for {setupFile}", setupFile);
+
+ var scriptFolder = Path.Combine(outputFolder, "wt_scripts");
+ var logsFolder = Path.Combine(outputFolder, "logs");
+
+ Directory.CreateDirectory(scriptFolder);
+ Directory.CreateDirectory(logsFolder);
+
+ if (!File.Exists(setupFile))
+ {
+ throw new FileNotFoundException("Installer file not found", setupFile);
+ }
+
+ var sandboxFile = Path.Combine(outputFolder, "sandbox.wsb");
+ await WriteSandboxConfig(sandboxFile, installerFolder, logsFolder, scriptFolder);
+ await WriteTestScript(scriptFolder, Path.GetFileName(setupFile), installerArguments, timeout);
+
+ return sandboxFile;
+ }
+
+ ///
+ /// Creates a Windows Sandbox configuration file
+ ///
+ /// Absolute path the the sandbox file
+ /// Where is the installer located, this folder will be mapped to the Sandbox as readonly
+ /// Where should the logs be placed? This folder will be mapped to the Sandbox as writable
+ /// Additional script folder if not in the installer folder
+ ///
+ private static async Task WriteSandboxConfig(string sandboxFilename, string installerFolder, string logFolder, string? scriptFolder = null)
+ {
+ var stringBuilder = new StringBuilder();
+ stringBuilder.AppendLine("");
+ // Mapped folders (installer and logs)
+ // some logs are parsed to show the actual result.
+ stringBuilder.AppendLine(" ");
+ stringBuilder.AppendLine(" ");
+ stringBuilder.AppendLine($" {installerFolder}");
+ stringBuilder.AppendLine(" c:\\Users\\WDAGUtilityAccount\\Downloads\\Wintuner");
+ stringBuilder.AppendLine(" true");
+ stringBuilder.AppendLine(" ");
+ stringBuilder.AppendLine(" ");
+ stringBuilder.AppendLine($" {logFolder}");
+ stringBuilder.AppendLine(" c:\\Users\\WDAGUtilityAccount\\Desktop\\logs");
+ stringBuilder.AppendLine(" false");
+ stringBuilder.AppendLine(" ");
+ if (scriptFolder is not null)
+ {
+ stringBuilder.AppendLine(" ");
+ stringBuilder.AppendLine($" {scriptFolder}");
+ stringBuilder.AppendLine(" c:\\Users\\WDAGUtilityAccount\\Downloads\\Wintuner\\wt_scripts");
+ stringBuilder.AppendLine(" true");
+ stringBuilder.AppendLine(" ");
+ }
+ stringBuilder.AppendLine(" ");
+
+ // Startup command
+ stringBuilder.AppendLine(" ");
+ stringBuilder.AppendLine(" c:\\Users\\WDAGUtilityAccount\\Downloads\\Wintuner\\wt_scripts\\startup.cmd");
+ stringBuilder.AppendLine(" ");
+
+ // Security settings
+ stringBuilder.AppendLine(" Disable");
+ stringBuilder.AppendLine(" Disable");
+ // Not sure about this next one https://learn.microsoft.com/en-us/windows/security/application-security/application-isolation/windows-sandbox/windows-sandbox-configure-using-wsb-file#protected-client
+ stringBuilder.AppendLine(" Enabled");
+ stringBuilder.AppendLine(" Disable");
+ stringBuilder.AppendLine(" Disable");
+ stringBuilder.AppendLine("");
+
+ await File.WriteAllTextAsync(sandboxFilename, stringBuilder.ToString());
+ }
+
+ ///
+ /// Creates the test script that will be executed in the sandbox
+ ///
+ /// Script folder location, it will create the scripts here.
+ /// Filename of the installer
+ /// Arguments of the installer, will be added to the install script
+ /// If a value above -1 is provided, 'shutdown /s /t {timeout}' is added to the install script
+ ///
+ private static async Task WriteTestScript(string scriptFolder, string installerFilename, string? installerArguments, int? timeout)
+ {
+
+ // Create a batch script that will run a powershell script (Execution policy stuff...)
+ var sb = new StringBuilder();
+ sb.AppendLine("@echo off");
+ sb.AppendLine("start /wait /low powershell.exe -ExecutionPolicy Bypass -File \"C:\\Users\\WDAGUtilityAccount\\Downloads\\Wintuner\\wt_scripts\\install.ps1\"");
+ sb.AppendLine();
+ Directory.CreateDirectory(scriptFolder);
+ await File.WriteAllTextAsync(Path.Combine(scriptFolder, "startup.cmd"), sb.ToString());
+
+ sb.Clear();
+
+ // Create the powershell script that will install the app
+ // and collect the installed apps
+ // This script will also shutdown the sandbox after the installation (if a timeout above -1 is provided)
+ sb.AppendLine("Start-Transcript -Path c:\\Users\\WDAGUtilityAccount\\Desktop\\logs\\wintuner.log -Append -Force");
+ sb.AppendLine("Write-Host \"Starting installation\"");
+ sb.AppendLine($"Write-Host \"Installer: {installerFilename}\"");
+ sb.AppendLine($"Write-Host \"Arguments: {installerArguments}\"");
+
+ // execute the installer and capture the exit code in powershell
+ //sb.AppendLine($"& \"c:\\Users\\WDAGUtilityAccount\\Downloads\\Wintuner\\{installerFilename}\" {installerArguments}");
+ //sb.AppendLine("& cmd exit /b 5"); // This is a dummy command to test the exit code
+ //sb.AppendLine("$exitCode = $LASTEXITCODE");
+
+ if (string.IsNullOrWhiteSpace(installerArguments))
+ {
+ sb.AppendLine($"$setupProcess = Start-Process -FilePath \"c:\\Users\\WDAGUtilityAccount\\Downloads\\Wintuner\\{installerFilename}\" -Wait -PassThru");
+ }
+ else
+ {
+ sb.AppendLine($"$setupProcess = Start-Process -FilePath \"c:\\Users\\WDAGUtilityAccount\\Downloads\\Wintuner\\{installerFilename}\" -ArgumentList \"{installerArguments}\" -Wait -PassThru");
+ }
+
+ sb.AppendLine("$exitCode = $setupProcess.ExitCode");
+ sb.AppendLine("Write-Host \"Installer finished with exitcode $exitCode\"");
+
+ // write the exit code to a file
+ sb.AppendLine("$exitCode | Out-File -FilePath c:\\Users\\WDAGUtilityAccount\\Desktop\\logs\\exitcode.txt");
+ sb.AppendLine("Write-Host \"App installed, collecting installed apps\"");
+ sb.AppendLine("$apps = $(Get-WmiObject -Class Win32_InstalledWin32Program | Select-Object -Property Version,Vendor,Name)");
+ sb.AppendLine("$apps | Format-Table -AutoSize");
+ sb.AppendLine("$apps | Export-Csv -Path c:\\Users\\WDAGUtilityAccount\\Desktop\\logs\\installed.csv -NoTypeInformation");
+ sb.AppendLine("Write-Host \"Installed apps collected\"");
+ sb.AppendLine("Stop-Transcript");
+ if (timeout is not null && timeout >= 0)
+ {
+ if (timeout > 0) // Cancelable shutdown
+ {
+ sb.AppendLine($"shutdown /s /t {timeout}");
+ sb.AppendLine($"Write-Host \"Closing sandbox in {timeout} seconds unless you press a button\"");
+ sb.AppendLine("Read-Host");
+ sb.AppendLine("shutdown /a");
+ }
+ else // Immediate shutdown
+ {
+ sb.AppendLine($"shutdown /s /t {timeout}");
+ }
+ }
+ // Exit with the exit code of the installer (not sure if that does anything)
+ sb.AppendLine("exit $exitCode");
+ await File.WriteAllTextAsync(Path.Combine(scriptFolder, "install.ps1"), sb.ToString());
+ }
+
+ ///
+ /// Runs a sandbox file
+ ///
+ /// Absolute path of the .wsb file
+ /// Should we try to cleanup the folder containing the sandbox file?
+ /// In case you want to cancel the process.
+ ///
+ public async Task RunSandbox(string sandboxFile, bool cleanup, CancellationToken cancellationToken)
+ {
+ logger.LogInformation("Running sandbox {sandboxFile}", sandboxFile);
+ var processResult = await processManager.RunProcessAsync("WindowsSandbox.exe", sandboxFile, cancellationToken);
+ logger.LogInformation("Sandbox exited with exitcode {exitCode}", processResult.ExitCode);
+ bool shouldProcess = true;
+ SandboxResult? result = null;
+ if (processResult.ExitCode == -2147024713)
+ {
+ logger.LogWarning("Sandbox failed to start, this is likely because the host does not support virtualization or already started");
+ shouldProcess = false;
+ }
+ await Task.Delay(1500, cancellationToken);
+
+ if (shouldProcess)
+ {
+ var logDirectory = Path.Combine(Path.GetDirectoryName(sandboxFile)!, "logs");
+ var logFile = Path.Combine(logDirectory, "wintuner.log");
+ var exitCodeFile = Path.Combine(logDirectory, "exitcode.txt");
+ result = new SandboxResult
+ {
+ ExitCode = File.Exists(exitCodeFile) && int.TryParse(await File.ReadAllTextAsync(exitCodeFile, cancellationToken), out int exitCode) ? exitCode : 0,
+ Log = File.Exists(logFile) ? await File.ReadAllTextAsync(logFile, cancellationToken) : null,
+ InstalledApps = await ParseInstalledApps(Path.Combine(logDirectory, "installed.csv"), cancellationToken)
+ };
+ }
+
+ if (cleanup)
+ {
+ logger.LogDebug("Cleaning up sandbox files");
+ Directory.Delete(Path.GetDirectoryName(sandboxFile)!, true);
+ }
+ return result;
+
+ }
+
+ private async Task?> ParseInstalledApps(string filename, CancellationToken cancellationToken = default)
+ {
+
+ // the file is a csv with headers Version,Vendor,Name and uses , as separator
+ // Parse the file if it exists
+ if (!File.Exists(filename))
+ {
+ logger.LogWarning("Installed apps file not found {filename}", filename);
+ return null;
+ }
+
+ var lines = await File.ReadAllLinesAsync(filename, cancellationToken);
+ if (lines.Length < 2)
+ {
+ logger.LogWarning("Installed apps file is empty {filename}", filename);
+ return null;
+ }
+
+ return lines.Skip(1).Select(l =>
+ {
+ var parts = l.Split(',');
+ return new SandboxInstalledApps
+ {
+ Version = parts[0].Trim('"'),
+ Vendor = parts[1].Trim('"'),
+ Name = parts[2].Trim('"')
+ };
+ });
+ }
+
+ public class SandboxResult
+ {
+ public int ExitCode { get; set; } = 0;
+ public string? Log { get; set; }
+ public IEnumerable? InstalledApps { get; set; }
+
+ public override string ToString()
+ {
+ return InstalledApps?.Count() > 0 ? $"ExitCode: {ExitCode}, Installed apps {string.Join(", ", InstalledApps.Select(i => i.Name))}" : $"Exit code: {ExitCode}";
+ }
+
+ }
+
+ public class SandboxInstalledApps
+ {
+ public string? Version { get; set; }
+ public string? Vendor { get; set; }
+ public string? Name { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Name} by {Vendor}";
+ }
+ }
+}
diff --git a/src/WingetIntune/WingetServiceCollectionExtension.cs b/src/WingetIntune/WingetServiceCollectionExtension.cs
index e5fc982..a68fd74 100644
--- a/src/WingetIntune/WingetServiceCollectionExtension.cs
+++ b/src/WingetIntune/WingetServiceCollectionExtension.cs
@@ -3,6 +3,7 @@
using Microsoft.Kiota.Http.HttpClientLibrary;
using WingetIntune.Interfaces;
using WingetIntune.Intune;
+using WingetIntune.Testing;
using WinTuner.Proxy.Client;
[assembly: InternalsVisibleTo("WingetIntune.Tests")]
namespace WingetIntune;
@@ -54,6 +55,7 @@ public static IServiceCollection AddWingetServices(this IServiceCollection servi
services.AddTransient();
services.AddSingleton();
services.AddSingleton();
+ services.AddTransient();
services.AddWinTunerProxyClient(config =>
{