Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1877,6 +1877,166 @@ public void BuildArguments_UsesInnoDefaultsWhenManifestOmitsSwitches()
Assert.Equal(["/SP-", "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"], InstallerDispatch.BuildArguments("inno", InstallerMode.Silent, installer));
}

[Theory]
[InlineData("nullsoft")]
[InlineData("nsis")]
[InlineData("NULLSOFT")]
[InlineData("NSIS")]
public void PrepareInstallerForExecution_PreservesVerifiedNullsoftBytes(string installerType)
{
var directory = TestPaths.CreateTempAppRoot();
Directory.CreateDirectory(directory);
var payload = "verified NSIS installer bytes"u8.ToArray();
try
{
foreach (var filename in new[] { "download", "download.php", "custom", "setup.exe", "setup.EXE" })
{
var original = Path.Combine(directory, filename);
File.WriteAllBytes(original, payload);
var expected = filename.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)
? original
: Path.ChangeExtension(original, ".exe");
if (original != expected)
File.WriteAllText(expected, "stale installer");

var prepared = Repository.PrepareInstallerForExecution(original, installerType);
Assert.Equal(expected, prepared);
Assert.Equal(payload, File.ReadAllBytes(prepared));
if (original != prepared)
Assert.False(File.Exists(original));
File.Delete(prepared);
}
}
finally
{
TestPaths.DeleteAppRoot(directory);
}
}

[Theory]
[InlineData("exe")]
[InlineData("inno")]
[InlineData("burn")]
[InlineData("msi")]
[InlineData("msix")]
[InlineData("zip")]
[InlineData("portable")]
public void PrepareInstallerForExecution_LeavesOtherTypesUnchanged(string installerType)
{
Assert.Equal("download", Repository.PrepareInstallerForExecution("download", installerType));
}

[Fact]
public void PrepareInstallerForExecution_ReportsMissingNullsoftPayload()
{
var directory = TestPaths.CreateTempAppRoot();
Directory.CreateDirectory(directory);
try
{
Assert.Throws<FileNotFoundException>(() =>
Repository.PrepareInstallerForExecution(Path.Combine(directory, "download"), "nullsoft"));
}
finally
{
TestPaths.DeleteAppRoot(directory);
}
}

[Fact]
public void BuildArguments_UsesNullsoftSilentSwitch()
{
var installer = new Installer();

Assert.Equal(["/S"], InstallerDispatch.BuildArguments("nullsoft", InstallerMode.SilentWithProgress, installer));
Assert.Equal(["/S"], InstallerDispatch.BuildArguments("nullsoft", InstallerMode.Silent, installer));
Assert.Equal(["/S"], InstallerDispatch.BuildArguments("nsis", InstallerMode.SilentWithProgress, installer));
Assert.Equal(["/S"], InstallerDispatch.BuildArguments("nsis", InstallerMode.Silent, installer));
}

[Fact]
public async Task InstallerCompletion_DoesNotWaitForDetachedChildProcess()
{
if (!OperatingSystem.IsWindows())
return;

var directory = Path.Combine(Path.GetTempPath(), $"pinget-process-lifecycle-{Guid.NewGuid():N}");
Directory.CreateDirectory(directory);
var childScriptPath = Path.Combine(directory, "child.ps1");
var launcherPath = Path.Combine(directory, "launcher.cmd");
var childPidPath = Path.Combine(directory, "child.pid");
var childPid = 0;
Task<int>? installTask = null;

try
{
var quotedPidPath = childPidPath.Replace("'", "''", StringComparison.Ordinal);
File.WriteAllText(
childScriptPath,
$"Set-Content -LiteralPath '{quotedPidPath}' -Value $PID{Environment.NewLine}Start-Sleep -Seconds 30");
File.WriteAllText(
launcherPath,
$"@echo off{Environment.NewLine}start \"\" /b powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"{childScriptPath}\"{Environment.NewLine}");

var commandInterpreter = Environment.GetEnvironmentVariable("ComSpec")
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
var request = new InstallRequest
{
Query = new PackageQuery(),
Mode = InstallerMode.Interactive,
Override = $"/c \"{launcherPath}\"",
};
installTask = Task.Run(() => InstallerDispatch.Execute(
commandInterpreter,
"exe",
request,
new Manifest { Id = "Test.Lifecycle", Name = "Lifecycle Test", Version = "1.0" },
new Installer()));

var exitCode = await installTask.WaitAsync(TimeSpan.FromSeconds(10));
Assert.Equal(0, exitCode);

// PowerShell cold start can take several seconds on loaded CI runners.
var markerDeadline = DateTime.UtcNow.AddSeconds(20);
while (childPid == 0 && DateTime.UtcNow < markerDeadline)
{
if (File.Exists(childPidPath))
int.TryParse(await File.ReadAllTextAsync(childPidPath), out childPid);

if (childPid == 0)
await Task.Delay(25);
}

Assert.NotEqual(0, childPid);
using var child = Process.GetProcessById(childPid);
Assert.False(child.HasExited, "The child must outlive the installer process whose exit is awaited.");
}
finally
{
if (childPid == 0 && File.Exists(childPidPath))
int.TryParse(await File.ReadAllTextAsync(childPidPath), out childPid);

if (childPid != 0)
{
try
{
using var child = Process.GetProcessById(childPid);
child.Kill(entireProcessTree: true);
child.WaitForExit(3000);
}
catch (ArgumentException) { }
catch (InvalidOperationException) { }
}

if (installTask is { IsCompleted: false })
{
try { await installTask.WaitAsync(TimeSpan.FromSeconds(2)); }
catch (TimeoutException) { }
}

Directory.Delete(directory, recursive: true);
}
}

[Fact]
public void BuildArguments_AppendsManifestAndCliSwitches()
{
Expand Down
14 changes: 14 additions & 0 deletions dotnet/src/Devolutions.Pinget.Core/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@ public InstallResult Install(InstallRequest request)
var installerPath = FetchInstaller(request, tempDir).InstallerPath;

var installerType = (selectedInstaller.InstallerType ?? "exe").ToLowerInvariant();
installerPath = PrepareInstallerForExecution(installerPath, installerType);
var exitCode = InstallerDispatch.Execute(installerPath, installerType, request, manifest, selectedInstaller);

return new InstallResult
Expand All @@ -1023,6 +1024,19 @@ public InstallResult Install(InstallRequest request)
};
}

internal static string PrepareInstallerForExecution(string installerPath, string installerType)
{
if (!(installerType.Equals("nullsoft", StringComparison.OrdinalIgnoreCase) ||
installerType.Equals("nsis", StringComparison.OrdinalIgnoreCase)) ||
Path.GetExtension(installerPath).Equals(".exe", StringComparison.OrdinalIgnoreCase))
return installerPath;

// Like WinGet, give the verified NSIS payload an executable extension before ShellExecute.
var executablePath = Path.ChangeExtension(installerPath, ".exe");
File.Move(installerPath, executablePath, overwrite: true);
return executablePath;
}

public InstallResult Repair(RepairRequest request)
{
if (string.IsNullOrWhiteSpace(request.ManifestPath) && !OperatingSystem.IsWindows())
Expand Down
Loading
Loading