Warning hunting; refactor process invoke

This commit is contained in:
Caelan Sayler
2022-05-07 14:03:00 +01:00
parent 344de3fb88
commit 0aac326f45
21 changed files with 407 additions and 247 deletions

View File

@@ -267,17 +267,18 @@ void extractSingleFile(void* zipBuf, size_t cZipBuf, wstring fileLocation, std::
throwLastMzError(&zip_archive, L"Unable to extract selected file from archive (DEFLATE).");
}
else if (file_stat.m_method == MZ_LZMA) {
// miniz does not support LZMA, so we will extract as compressed data
// using MZ_ZIP_FLAG_COMPRESSED_DATA and then decode after using LZMA SDK
auto dataCompr = std::vector<Byte>(file_stat.m_comp_size);
if (!mz_zip_reader_extract_to_mem(&zip_archive, file_stat.m_file_index, &dataCompr[0], file_stat.m_comp_size, MZ_ZIP_FLAG_COMPRESSED_DATA))
auto dataCompr = std::vector<Byte>((size_t)file_stat.m_comp_size);
if (!mz_zip_reader_extract_to_mem(&zip_archive, file_stat.m_file_index, &dataCompr[0], (size_t)file_stat.m_comp_size, MZ_ZIP_FLAG_COMPRESSED_DATA))
throwLastMzError(&zip_archive, L"Unable to extract selected file from archive (LZMA).");
// LZMA stream in zip container starts with 4 bytes: 0x09, 0x14, 0x05, 0x00
// after that, there are 5 bytes that make up the LZMA decode properties
size_t szCompr = file_stat.m_comp_size - LZMA_PROPS_SIZE - 4;
size_t szDecompr = file_stat.m_uncomp_size;
auto dataDecompr = std::vector<Byte>(file_stat.m_uncomp_size);
size_t szCompr = (size_t)file_stat.m_comp_size - LZMA_PROPS_SIZE - 4;
size_t szDecompr = (size_t)file_stat.m_uncomp_size;
auto dataDecompr = std::vector<Byte>((size_t)file_stat.m_uncomp_size);
ELzmaStatus status;
SRes lzr = LzmaDecode(&dataDecompr[0], &szDecompr, &dataCompr[LZMA_PROPS_SIZE + 4],

View File

@@ -13,6 +13,7 @@
<PathMap>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=./</PathMap>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
<NoWarn>CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@@ -115,7 +115,7 @@ namespace Squirrel.CommandLine
return HostWriter.IsBundle(peFile, out var offset) && offset > 0;
}
public static async Task UpdateSingleFileBundleIcon(string rootTempDir, string sourceFile, string destinationFile, string iconPath)
public static void UpdateSingleFileBundleIcon(string rootTempDir, string sourceFile, string destinationFile, string iconPath)
{
using var _ = Utility.GetTempDir(rootTempDir, out var tmpdir);
var sourceName = Path.GetFileNameWithoutExtension(sourceFile);
@@ -136,7 +136,7 @@ namespace Squirrel.CommandLine
// set new icon
Log.Info("Patching Update.exe icon");
await HelperExe.SetExeIcon(newAppHost, iconPath);
HelperExe.SetExeIcon(newAppHost, iconPath);
// create new bundle
var bundlerOutput = Path.Combine(tmpdir, "output");
@@ -153,7 +153,7 @@ namespace Squirrel.CommandLine
);
Log.Info("Re-packing Update.exe bundle");
var singleFile = DotnetUtil.GenerateBundle(bundler, tmpdir, bundlerOutput);
var singleFile = GenerateBundle(bundler, tmpdir, bundlerOutput);
// copy to requested location
File.Copy(singleFile, destinationFile);

View File

@@ -1,9 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Squirrel.Lib;
namespace Squirrel.CommandLine
{
@@ -11,19 +15,81 @@ namespace Squirrel.CommandLine
internal class HelperExe : HelperFile
{
public static string SetupPath => FindHelperFile("Setup.exe");
public static string UpdatePath
public static string UpdatePath
=> FindHelperFile("Update.exe", p => Microsoft.NET.HostModel.AppHost.HostWriter.IsBundle(p, out var _));
public static string StubExecutablePath => FindHelperFile("StubExecutable.exe");
public static string SingleFileHostPath => FindHelperFile("singlefilehost.exe");
public static string SignToolPath => FindHelperFile("signtool.exe");
// private so we don't expose paths to internal tools. these should be exposed as a helper function
private static string SignToolPath => FindHelperFile("signtool.exe");
private static string WixTemplatePath => FindHelperFile("template.wxs");
private static string RceditPath => FindHelperFile("rcedit.exe");
private static string WixCandlePath => FindHelperFile("candle.exe");
private static string WixLightPath => FindHelperFile("light.exe");
public static async Task<string> CompileWixTemplateToMsi(Dictionary<string, string> templateData, string workingDir, string appId)
private static bool CheckIsAlreadySigned(string filePath)
{
if (String.IsNullOrWhiteSpace(filePath)) return true;
if (!File.Exists(filePath)) {
Log.Warn($"Cannot sign '{filePath}', file does not exist.");
return true;
}
try {
if (AuthenticodeTools.IsTrusted(filePath)) {
Log.Debug("'{0}' is already signed, skipping...", filePath);
return true;
}
} catch (Exception ex) {
Log.ErrorException("Failed to determine signing status for " + filePath, ex);
}
return false;
}
public static void SignPEFilesWithSignTool(string filePath, string signArguments)
{
if (CheckIsAlreadySigned(filePath)) return;
List<string> args = new List<string>();
args.Add("sign");
args.AddRange(NativeMethods.CommandLineToArgvW(signArguments));
args.Add(filePath);
var result = ProcessUtil.InvokeProcess(SignToolPath, args, null, CancellationToken.None);
if (result.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
throw new Exception(
$"Command failed:\n{cmdWithPasswordHidden}\n\n" +
$"Output was:\n" + result.StdOutput);
} else {
Log.Info("Sign successful: " + result.StdOutput);
}
}
public static void SignPEFilesWithTemplate(string filePath, string signTemplate)
{
if (CheckIsAlreadySigned(filePath)) return;
var command = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\"");
var args = NativeMethods.CommandLineToArgvW(command);
if (args.Length < 2)
throw new OptionValidationException("Invalid signing template");
var result = ProcessUtil.InvokeProcess(args[0], args.Skip(1), null, CancellationToken.None);
if (result.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
throw new Exception(
$"Command failed:\n{cmdWithPasswordHidden}\n\n" +
$"Output was:\n" + result.StdOutput);
} else {
Log.Info("Sign successful: " + result.StdOutput);
}
}
public static string CompileWixTemplateToMsi(Dictionary<string, string> templateData, string workingDir, string appId)
{
var wxsFile = Path.Combine(workingDir, appId + ".wxs");
var objFile = Path.Combine(workingDir, appId + ".wixobj");
@@ -38,12 +104,12 @@ namespace Squirrel.CommandLine
// Candle reprocesses and compiles WiX source files into object files (.wixobj).
Log.Info("Compiling WiX Template (candle.exe)");
var candleParams = new string[] { "-nologo", "-ext", "WixNetFxExtension", "-out", objFile, wxsFile };
await InvokeAndThrowIfNonZero(WixCandlePath, candleParams, workingDir).ConfigureAwait(false);
InvokeAndThrowIfNonZero(WixCandlePath, candleParams, workingDir);
// Light links and binds one or more .wixobj files and creates a Windows Installer database (.msi or .msm).
Log.Info("Linking WiX Template (light.exe)");
var lightParams = new string[] { "-ext", "WixNetFxExtension", "-spdb", "-sval", "-out", msiFile, objFile };
await InvokeAndThrowIfNonZero(WixLightPath, lightParams, workingDir).ConfigureAwait(false);
InvokeAndThrowIfNonZero(WixLightPath, lightParams, workingDir);
return msiFile;
} finally {
Utility.DeleteFileOrDirectoryHard(wxsFile, throwOnFailure: false);
@@ -51,14 +117,14 @@ namespace Squirrel.CommandLine
}
}
public static Task SetExeIcon(string exePath, string iconPath)
public static void SetExeIcon(string exePath, string iconPath)
{
Log.Info("Updating PE icon for: " + exePath);
var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath };
return InvokeAndThrowIfNonZero(RceditPath, args);
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
}
public static Task SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPackage package, string iconPath = null)
public static void SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPackage package, string iconPath = null)
{
Log.Info("Updating StringTable resources for: " + exePath);
var realExePath = Path.GetFullPath(exePath);
@@ -78,7 +144,7 @@ namespace Squirrel.CommandLine
args.Add(Path.GetFullPath(iconPath));
}
return InvokeAndThrowIfNonZero(RceditPath, args);
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
}
}
}

View File

@@ -19,6 +19,17 @@ namespace Squirrel.CommandLine
v => signTemplate = v);
}
public void SignPEFile(string filePath)
{
if (!String.IsNullOrEmpty(signParams)) {
HelperExe.SignPEFilesWithSignTool(filePath, signParams);
} else if (!String.IsNullOrEmpty(signTemplate)) {
HelperExe.SignPEFilesWithTemplate(filePath, signTemplate);
} else {
Log.Debug($"No signing paramaters, file will not be signed: '{filePath}'.");
}
}
public override void Validate()
{
if (!String.IsNullOrEmpty(signParams) && !String.IsNullOrEmpty(signTemplate)) {
@@ -29,44 +40,6 @@ namespace Squirrel.CommandLine
throw new OptionValidationException($"Argument 'signTemplate': Must contain '{{{{file}}}}' in template string (replaced with the file to sign). Current value is '{signTemplate}'");
}
}
public void SignPEFile(string filePath)
{
try {
if (AuthenticodeTools.IsTrusted(filePath)) {
Log.Debug("'{0}' is already signed, skipping...", filePath);
return;
}
} catch (Exception ex) {
Log.ErrorException("Failed to determine signing status for " + filePath, ex);
}
string cmd;
ProcessStartInfo psi;
if (!String.IsNullOrEmpty(signParams)) {
// use embedded signtool.exe with provided parameters
cmd = $"sign {signParams} \"{filePath}\"";
psi = Utility.CreateProcessStartInfo(HelperExe.SignToolPath, cmd);
cmd = "signtool.exe " + cmd;
} else if (!String.IsNullOrEmpty(signTemplate)) {
// escape custom sign command and pass it to cmd.exe
cmd = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\"");
psi = Utility.CreateProcessStartInfo("cmd", $"/c {Utility.EscapeCmdExeMetachars(cmd)}");
} else {
Log.Debug("{0} was not signed. (skipped; no signing parameters)", filePath);
return;
}
var processResult = Utility.InvokeProcessUnsafeAsync(psi, CancellationToken.None)
.ConfigureAwait(false).GetAwaiter().GetResult();
if (processResult.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"/p\s+\w+").Replace(cmd, "/p ********");
throw new Exception("Signing command failed: \n > " + cmdWithPasswordHidden + "\n" + processResult.StdOutput);
} else {
Log.Info("Sign successful: " + processResult.StdOutput);
}
}
}
internal class ReleasifyOptions : SigningOptions

View File

@@ -70,7 +70,7 @@ namespace Squirrel.CommandLine
var bundledUpdatePath = HelperExe.UpdatePath;
var updatePath = Path.Combine(tempDir, "Update.exe");
if (setupIcon != null) {
DotnetUtil.UpdateSingleFileBundleIcon(TempDir, bundledUpdatePath, updatePath, setupIcon).Wait();
DotnetUtil.UpdateSingleFileBundleIcon(TempDir, bundledUpdatePath, updatePath, setupIcon);
} else {
File.Copy(bundledUpdatePath, updatePath, true);
}
@@ -262,7 +262,7 @@ namespace Squirrel.CommandLine
var bundledzp = new ZipPackage(package);
var targetSetupExe = Path.Combine(di.FullName, $"{bundledzp.Id}Setup.exe");
File.Copy(options.debugSetupExe ?? HelperExe.SetupPath, targetSetupExe, true);
Utility.Retry(() => HelperExe.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledzp, setupIcon).Wait());
HelperExe.SetPEVersionBlockFromPackageInfo(targetSetupExe, bundledzp, setupIcon);
var newestFullRelease = Squirrel.EnumerableExtensions.MaxBy(releaseEntries, x => x.Version).Where(x => !x.IsDelta).First();
var newestReleasePath = Path.Combine(di.FullName, newestFullRelease.Filename);
@@ -282,14 +282,14 @@ namespace Squirrel.CommandLine
if (!String.IsNullOrEmpty(options.msi)) {
bool x64 = options.msi.Equals("x64");
var msiPath = createMsiPackage(targetSetupExe, bundledzp, x64).Result;
var msiPath = createMsiPackage(targetSetupExe, bundledzp, x64);
options.SignPEFile(msiPath);
}
Log.Info("Done");
}
static Task<string> createMsiPackage(string setupExe, IPackage package, bool packageAs64Bit)
static string createMsiPackage(string setupExe, IPackage package, bool packageAs64Bit)
{
Log.Info($"Compiling machine-wide msi deployment tool in {(packageAs64Bit ? "64-bit" : "32-bit")} mode");

View File

@@ -163,7 +163,7 @@ namespace Squirrel.CommandLine
printProcessed(newLibFiles.Length, baseLibFiles.Count);
ReleasePackageBuilder.addDeltaFilesToContentTypes(tempInfo.FullName);
HelperFile.CompressLzma7z(outputFile, tempInfo.FullName).GetAwaiterResult();
HelperFile.CompressLzma7z(outputFile, tempInfo.FullName);
this.Log().Info(
$"Successfully created delta package for {basePackage.Version} -> {newPackage.Version}" +

View File

@@ -50,8 +50,7 @@ namespace Squirrel.CommandLine
if (helper != null)
return helper;
var psi = Utility.CreateProcessStartInfo(findCommand, n);
var result = Utility.InvokeProcessUnsafeAsync(psi, CancellationToken.None).GetAwaiterResult();
var result = ProcessUtil.InvokeProcess(findCommand, new[] { n }, null, CancellationToken.None);
if (result.ExitCode == 0) {
return n;
}
@@ -87,19 +86,19 @@ namespace Squirrel.CommandLine
return result;
}
public static async Task CompressLzma7z(string zipFilePath, string inFolder)
public static void CompressLzma7z(string zipFilePath, string inFolder)
{
Log.Info($"Compressing '{inFolder}' to '{zipFilePath}' using 7z (LZMA)...");
var args = new string[] { "a", zipFilePath, "-tzip", "-m0=LZMA", "-aoa", "-y", "*" };
await InvokeAndThrowIfNonZero(SevenZipPath, args, inFolder).ConfigureAwait(false);
InvokeAndThrowIfNonZero(SevenZipPath, args, inFolder);
}
protected static async Task InvokeAndThrowIfNonZero(string exePath, IEnumerable<string> args, string workingDir = null)
protected static void InvokeAndThrowIfNonZero(string exePath, IEnumerable<string> args, string workingDir)
{
var result = await Utility.InvokeProcessAsync(exePath, args, CancellationToken.None, workingDir).ConfigureAwait(false);
var result = ProcessUtil.InvokeProcess(exePath, args, workingDir, CancellationToken.None);
if (result.ExitCode != 0) {
throw new Exception(
$"Command failed: \n{Path.GetFileName(exePath)} {Utility.ArgsToCommandLine(args)}\n\n" +
$"Command failed:\n{result.Command}\n\n" +
$"Output was:\n" + result.StdOutput);
}
}

View File

@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
@@ -70,7 +70,7 @@ namespace Microsoft.NET.HostModel.AppHost
{
if (assemblyToCopyResorcesFrom != null && appHostIsPEImage)
{
if (ResourceUpdater.IsSupportedOS())
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && ResourceUpdater.IsSupportedOS())
{
// Copy resources from managed dll to the apphost
new ResourceUpdater(appHostDestinationFilePath)

View File

@@ -115,7 +115,7 @@ namespace Squirrel.CommandLine
contentsPostProcessHook?.Invoke(tempPath, package);
HelperFile.CompressLzma7z(outputFile, tempPath).GetAwaiterResult();
HelperFile.CompressLzma7z(outputFile, tempPath);
ReleasePackageFile = outputFile;
return ReleasePackageFile;

View File

@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@@ -144,17 +144,15 @@ namespace Squirrel
bool IsInstalledApp { get; }
/// <summary>The directory the app is (or will be) installed in.</summary>
//string AppDirectory { get; }
string AppDirectory { get; }
/// <summary>
/// Gets the currently installed version of the given executable, or if
/// not given, the currently running assembly
/// </summary>
/// <param name="executable">The executable to check, or null for this
/// executable</param>
/// <returns>The running version, or null if this is not a Squirrel
/// installed app (i.e. you're running from VS)</returns>
//SemanticVersion CurrentlyInstalledVersion(string executable = null);
SemanticVersion CurrentlyInstalledVersion();
/// <summary>
/// Creates an entry in Programs and Features based on the currently

View File

@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
@@ -30,6 +31,35 @@ namespace Squirrel
return (int) pbi.InheritedFromUniqueProcessId;
}
[DllImport("kernel32.dll", EntryPoint = "LocalFree", SetLastError = true)]
private static extern IntPtr _LocalFree(IntPtr hMem);
[DllImport("shell32.dll", EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
public static string[] CommandLineToArgvW(string cmdLine)
{
IntPtr argv = IntPtr.Zero;
try {
int numArgs = 0;
argv = _CommandLineToArgvW(cmdLine, out numArgs);
if (argv == IntPtr.Zero) {
throw new Win32Exception();
}
var result = new string[numArgs];
for (int i = 0; i < numArgs; i++) {
IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr)));
result[i] = Marshal.PtrToStringUni(currArg);
}
return result;
} finally {
_LocalFree(argv);
}
}
[DllImport("shell32.dll", SetLastError = true)]
public static extern void SetCurrentProcessExplicitAppUserModelID([MarshalAs(UnmanagedType.LPWStr)] string AppID);

View File

@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Squirrel
{
internal static class ProcessUtil
{
/*
* caesay 09/12/2021 at 12:10 PM
* yeah
* can I steal this for squirrel?
* Roman 09/12/2021 at 12:10 PM
* sure :)
* reference CommandRunner.cs on the github url as source? :)
* https://github.com/RT-Projects/RT.Util/blob/ef660cd693f66bc946da3aaa368893b03b74eed7/RT.Util.Core/CommandRunner.cs#L327
*/
/// <summary>
/// Given a number of argument strings, constructs a single command line string with all the arguments escaped
/// correctly so that a process using standard Windows API for parsing the command line will receive exactly the
/// strings passed in here. See Remarks.</summary>
/// <remarks>
/// The string is only valid for passing directly to a process. If the target process is invoked by passing the
/// process name + arguments to cmd.exe then further escaping is required, to counteract cmd.exe's interpretation
/// of additional special characters. See <see cref="EscapeCmdExeMetachars"/>.</remarks>
[SupportedOSPlatform("windows")]
private static string ArgsToCommandLine(IEnumerable<string> args)
{
var sb = new StringBuilder();
foreach (var arg in args) {
if (arg == null)
continue;
if (sb.Length != 0)
sb.Append(' ');
// For details, see https://web.archive.org/web/20150318010344/http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
// or https://devblogs.microsoft.com/oldnewthing/?p=12833
if (arg.Length != 0 && arg.IndexOfAny(_cmdChars) < 0)
sb.Append(arg);
else {
sb.Append('"');
for (int c = 0; c < arg.Length; c++) {
int backslashes = 0;
while (c < arg.Length && arg[c] == '\\') {
c++;
backslashes++;
}
if (c == arg.Length) {
sb.Append('\\', backslashes * 2);
break;
} else if (arg[c] == '"') {
sb.Append('\\', backslashes * 2 + 1);
sb.Append('"');
} else {
sb.Append('\\', backslashes);
sb.Append(arg[c]);
}
}
sb.Append('"');
}
}
return sb.ToString();
}
private static readonly char[] _cmdChars = new[] { ' ', '"', '\n', '\t', '\v' };
/// <summary>
/// Escapes all cmd.exe meta-characters by prefixing them with a ^. See <see cref="ArgsToCommandLine"/> for more
/// information.</summary>
[SupportedOSPlatform("windows")]
private static string EscapeCmdExeMetachars(string command)
{
var result = new StringBuilder();
foreach (var ch in command) {
switch (ch) {
case '(':
case ')':
case '%':
case '!':
case '^':
case '"':
case '<':
case '>':
case '&':
case '|':
result.Append('^');
break;
}
result.Append(ch);
}
return result.ToString();
}
private static string ArgsToCommandLineUnix(IEnumerable<string> args)
{
var sb = new StringBuilder();
foreach (var arg in args) {
if (arg == null)
continue;
if (sb.Length != 0)
sb.Append(' ');
// there are just too many 'command chars' in unix, so we play it
// super safe here and escape the string if there are any non-alpha-numeric
if (System.Text.RegularExpressions.Regex.IsMatch(arg, @"$[\w]^")) {
sb.Append(arg);
} else {
// https://stackoverflow.com/a/33949338/184746
// single quotes are 'strong quotes' and can contain everything
// except never other single quotes.
sb.Append("'");
sb.Append(arg.Replace("'", @"'\''"));
sb.Append("'");
}
}
return sb.ToString();
}
private static ProcessStartInfo CreateProcessStartInfo(string fileName, string workingDirectory)
{
var psi = new ProcessStartInfo(fileName);
psi.UseShellExecute = false;
psi.WindowStyle = ProcessWindowStyle.Hidden;
psi.ErrorDialog = false;
psi.CreateNoWindow = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory;
return psi;
}
private static (ProcessStartInfo StartInfo, string CommandDisplayString) CreateProcessStartInfo(string fileName, IEnumerable<string> args, string workingDirectory)
{
var psi = CreateProcessStartInfo(fileName, workingDirectory);
string displayArgs;
#if NET5_0_OR_GREATER
foreach (var a in args) psi.ArgumentList.Add(a);
displayArgs = $"['{String.Join("', '", args)}']";
#else
psi.Arguments = displayArgs = SquirrelRuntimeInfo.IsWindows ? ArgsToCommandLine(args) : ArgsToCommandLineUnix(args);
#endif
return (psi, fileName + " " + displayArgs);
}
private static (int ExitCode, string StdOutput) InvokeProcess(ProcessStartInfo psi, CancellationToken ct)
{
var pi = Process.Start(psi);
while (!ct.IsCancellationRequested) {
if (pi.WaitForExit(500)) break;
}
if (ct.IsCancellationRequested && !pi.HasExited) {
pi.Kill();
ct.ThrowIfCancellationRequested();
}
string output = pi.StandardOutput.ReadToEnd();
string error = pi.StandardError.ReadToEnd();
var all = (output ?? "") + Environment.NewLine + (error ?? "");
return (pi.ExitCode, all.Trim());
}
public static Process StartNonBlocking(string fileName, IEnumerable<string> args, string workingDirectory)
{
var (psi, cmd) = CreateProcessStartInfo(fileName, args, workingDirectory);
return Process.Start(psi);
}
public static (int ExitCode, string StdOutput, string Command) InvokeProcess(string fileName, IEnumerable<string> args, string workingDirectory, CancellationToken ct)
{
var (psi, cmd) = CreateProcessStartInfo(fileName, args, workingDirectory);
var p = InvokeProcess(psi, ct);
return (p.ExitCode, p.StdOutput, cmd);
}
//public static (int ExitCode, string StdOutput, string Command) InvokeProcess(string fileName, string args, string workingDirectory, CancellationToken ct)
//{
// var psi = CreateProcessStartInfo(fileName, workingDirectory);
// psi.Arguments = args;
// var p = InvokeProcess(psi, ct);
// return (p.ExitCode, p.StdOutput, fileName + " " + args);
//}
public static Task<(int ExitCode, string StdOutput, string Command)> InvokeProcessAsync(string fileName, IEnumerable<string> args, string workingDirectory, CancellationToken ct)
{
return Task.Run(() => InvokeProcess(fileName, args, workingDirectory, ct));
}
}
}

View File

@@ -226,103 +226,7 @@ namespace Squirrel
}
}
/*
* caesay 09/12/2021 at 12:10 PM
* yeah
* can I steal this for squirrel?
* Roman 09/12/2021 at 12:10 PM
* sure :)
* reference CommandRunner.cs on the github url as source? :)
* https://github.com/RT-Projects/RT.Util/blob/ef660cd693f66bc946da3aaa368893b03b74eed7/RT.Util.Core/CommandRunner.cs#L327
*/
/// <summary>
/// Given a number of argument strings, constructs a single command line string with all the arguments escaped
/// correctly so that a process using standard Windows API for parsing the command line will receive exactly the
/// strings passed in here. See Remarks.</summary>
/// <remarks>
/// The string is only valid for passing directly to a process. If the target process is invoked by passing the
/// process name + arguments to cmd.exe then further escaping is required, to counteract cmd.exe's interpretation
/// of additional special characters. See <see cref="EscapeCmdExeMetachars"/>.</remarks>
[SupportedOSPlatform("windows")]
public static string ArgsToCommandLine(IEnumerable<string> args)
{
var sb = new StringBuilder();
foreach (var arg in args) {
if (arg == null)
continue;
if (sb.Length != 0)
sb.Append(' ');
// For details, see https://web.archive.org/web/20150318010344/http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
// or https://devblogs.microsoft.com/oldnewthing/?p=12833
if (arg.Length != 0 && arg.IndexOfAny(_cmdChars) < 0)
sb.Append(arg);
else {
sb.Append('"');
for (int c = 0; c < arg.Length; c++) {
int backslashes = 0;
while (c < arg.Length && arg[c] == '\\') {
c++;
backslashes++;
}
if (c == arg.Length) {
sb.Append('\\', backslashes * 2);
break;
} else if (arg[c] == '"') {
sb.Append('\\', backslashes * 2 + 1);
sb.Append('"');
} else {
sb.Append('\\', backslashes);
sb.Append(arg[c]);
}
}
sb.Append('"');
}
}
return sb.ToString();
}
private static readonly char[] _cmdChars = new[] { ' ', '"', '\n', '\t', '\v' };
/// <summary>
/// Escapes all cmd.exe meta-characters by prefixing them with a ^. See <see cref="ArgsToCommandLine"/> for more
/// information.</summary>
[SupportedOSPlatform("windows")]
public static string EscapeCmdExeMetachars(string command)
{
var result = new StringBuilder();
foreach (var ch in command) {
switch (ch) {
case '(':
case ')':
case '%':
case '!':
case '^':
case '"':
case '<':
case '>':
case '&':
case '|':
result.Append('^');
break;
}
result.Append(ch);
}
return result.ToString();
}
/// <summary>
/// This function will escape command line arguments such that CommandLineToArgvW is guarenteed to produce the same output as the 'args' parameter.
/// It also will automatically execute wine if trying to run an exe while not on windows.
/// </summary>
[SupportedOSPlatform("windows")]
public static Task<(int ExitCode, string StdOutput)> InvokeProcessAsync(string fileName, IEnumerable<string> args, CancellationToken ct, string workingDirectory = "")
{
if (Environment.OSVersion.Platform != PlatformID.Win32NT && fileName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) {
return InvokeProcessUnsafeAsync(CreateProcessStartInfo("wine", ArgsToCommandLine(new string[] { fileName }.Concat(args)), workingDirectory), ct);
} else {
return InvokeProcessUnsafeAsync(CreateProcessStartInfo(fileName, ArgsToCommandLine(args), workingDirectory), ct);
}
}
public static T GetAwaiterResult<T>(this Task<T> task)
{
@@ -334,45 +238,6 @@ namespace Squirrel
task.ConfigureAwait(false).GetAwaiter().GetResult();
}
public static ProcessStartInfo CreateProcessStartInfo(string fileName, string arguments, string workingDirectory = "")
{
var psi = new ProcessStartInfo(fileName, arguments);
psi.UseShellExecute = false;
psi.WindowStyle = ProcessWindowStyle.Hidden;
psi.ErrorDialog = false;
psi.CreateNoWindow = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.WorkingDirectory = workingDirectory;
return psi;
}
public static async Task<(int ExitCode, string StdOutput)> InvokeProcessUnsafeAsync(ProcessStartInfo psi, CancellationToken ct)
{
var pi = Process.Start(psi);
await Task.Run(() => {
while (!ct.IsCancellationRequested) {
if (pi.WaitForExit(2000)) return;
}
if (ct.IsCancellationRequested) {
pi.Kill();
ct.ThrowIfCancellationRequested();
}
}).ConfigureAwait(false);
string textResult = await pi.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
if (String.IsNullOrWhiteSpace(textResult) || pi.ExitCode != 0) {
textResult = (textResult ?? "") + "\n" + await pi.StandardError.ReadToEndAsync().ConfigureAwait(false);
if (String.IsNullOrWhiteSpace(textResult)) {
textResult = String.Empty;
}
}
return (pi.ExitCode, textResult.Trim());
}
public static Task ForEachAsync<T>(this IEnumerable<T> source, Action<T> body, int degreeOfParallelism = 4)
{
return ForEachAsync(source, x => Task.Run(() => body(x)), degreeOfParallelism);

View File

@@ -1,4 +1,8 @@
// File generated by dotnet-combine at 2022-05-07__11_02_05
#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
#pragma warning disable CS0162 // Unreachable code detected
// File generated by dotnet-combine at 2022-05-07__11_02_05
// plist-cil - An open source library to parse and generate property lists for .NET
// Copyright (C) 2015 Natalia Portillo
@@ -938,8 +942,8 @@ namespace Squirrel.PropertyList
/// <summary>Parses a binary property list from a byte array.</summary>
/// <param name="data">The binary property list's data.</param>
/// <param name="offset">The length of the property list.</param>
/// <param name="count">The offset at which to start reading the property list.</param>
/// <param name="offset">The offset at which to start reading the property list.</param>
/// <param name="length">The length of the property list.</param>
/// <returns>The root object of the property list. This is usually a NSDictionary but can also be a NSArray.</returns>
/// <exception cref="PropertyListFormatException">When the property list's format could not be parsed.</exception>
public static NSObject Parse(byte[] data, int offset, int length) => Parse(data.AsSpan(offset, length));
@@ -1467,6 +1471,7 @@ namespace Squirrel.PropertyList
{
internal partial class BinaryPropertyListWriter
{
/// <summary>
/// The equality comparer which is used when adding an object to the <see cref="BinaryPropertyListWriter.idMap" />
/// . In most cases, objects are always added. The only exception are very specific strings, which are only added once.
@@ -4943,8 +4948,8 @@ namespace Squirrel.PropertyList
/// <summary>Parses a property list from a byte array.</summary>
/// <param name="bytes">The property list data as a byte array.</param>
/// <param name="offset">The length of the property list.</param>
/// <param name="count">The offset at which to start reading the property list.</param>
/// <param name="length">The length of the property list.</param>
/// <param name="offset">The offset at which to start reading the property list.</param>
/// <returns>The root object in the property list. This is usually a NSDictionary but can also be a NSArray.</returns>
public static NSObject Parse(byte[] bytes, int offset, int length) => Parse(bytes.AsSpan(offset, length));
@@ -5184,7 +5189,6 @@ namespace Squirrel.PropertyList
/// Initializes a new instance of the <see cref="Squirrel.PropertyList.UID" /> class using an unsigned 16-bit
/// number.
/// </summary>
/// <param name="name">Name.</param>
/// <param name="number">Unsigned 16-bit number.</param>
public UID(ushort number) => value = number;

View File

@@ -88,7 +88,7 @@ namespace Squirrel
var args = new string[] { "/passive", "/norestart", "/showrmui" };
var quietArgs = new string[] { "/q", "/norestart" };
Log.Info($"Running {Id} installer '{pathToInstaller} {string.Join(" ", args)}'");
var p = await Utility.InvokeProcessAsync(pathToInstaller, isQuiet ? quietArgs : args, CancellationToken.None).ConfigureAwait(false);
var p = await ProcessUtil.InvokeProcessAsync(pathToInstaller, isQuiet ? quietArgs : args, null, CancellationToken.None).ConfigureAwait(false);
// https://johnkoerner.com/install/windows-installer-error-codes/

View File

@@ -14,26 +14,42 @@ namespace Squirrel
/// <summary> The unique application Id. This is used in various app paths. </summary>
public virtual string AppId { get; }
/// <summary>
/// The root directory of the application. On Windows, this folder contains all
/// the application files, but that may not be the case on other operating systems.
/// </summary>
public virtual string RootAppDir { get; }
/// <summary> The directory in which nupkg files are stored for this application. </summary>
public virtual string PackagesDir { get; }
/// <summary> The temporary directory for this application. </summary>
public virtual string TempDir { get; }
/// <summary> The directory where new versions are stored, before they are applied. </summary>
public virtual string VersionStagingDir { get; }
/// <summary>
/// The directory where the current version of the application is stored.
/// This directory will be swapped out for a new version in <see cref="VersionStagingDir"/>.
/// </summary>
public virtual string CurrentVersionDir { get; }
/// <summary> The path to the current Update.exe or similar on other operating systems. </summary>
public virtual string UpdateExePath { get; }
/// <summary> The path to the RELEASES index detailing the local packages. </summary>
public virtual string ReleasesFilePath => Path.Combine(PackagesDir, "RELEASES");
/// <summary> The path to the .betaId file which contains a unique GUID for this user. </summary>
public virtual string BetaIdFilePath => Path.Combine(PackagesDir, ".betaId");
/// <summary> The currently installed version of the application. </summary>
public virtual SemanticVersion CurrentlyInstalledVersion => GetCurrentlyInstalledVersion();
private static IFullLogger Log() => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(UpdateConfig));
/// <summary> Creates a new instance of UpdateConfig. </summary>
public UpdateConfig(string applicationIdOverride, string localAppDataDirOverride)
{
UpdateExePath = GetUpdateExe();

View File

@@ -42,7 +42,7 @@ namespace Squirrel
// Progress range: 60 -> 80
// extracts the new package to a version dir (app-{ver}) inside VersionStagingDir
var newVersionStagingDir = await this.ErrorIfThrows(() => installPackageToStagingDir(updateInfo, release, x => progress(CalculateProgress(x, 60, 80))),
var newVersionDir = await this.ErrorIfThrows(() => installPackageToStagingDir(updateInfo, release, x => progress(CalculateProgress(x, 60, 80))),
"Failed to install package to app dir").ConfigureAwait(false);
progress(80);
@@ -50,14 +50,16 @@ namespace Squirrel
this.Log().Info("Updating local release file");
var currentReleases = await Task.Run(() => ReleaseEntry.BuildReleasesFile(_config.PackagesDir)).ConfigureAwait(false);
progress(85);
if (SquirrelRuntimeInfo.IsWindows) {
progress(85);
this.Log().Info("Running post-install hooks");
var currentVersionDir = await invokePostInstall(newVersionStagingDir, attemptingFullInstall, false, silentInstall).ConfigureAwait(false);
this.Log().Info("Running post-install hooks");
newVersionDir = await invokePostInstall(newVersionDir, attemptingFullInstall, false, silentInstall).ConfigureAwait(false);
progress(90);
progress(90);
executeSelfUpdate(currentVersionDir);
executeSelfUpdate(newVersionDir);
}
progress(95);
@@ -69,7 +71,7 @@ namespace Squirrel
progress(100);
return currentVersionDir;
return newVersionDir;
}
/// <inheritdoc/>
@@ -96,7 +98,8 @@ namespace Squirrel
using (var cts = new CancellationTokenSource()) {
cts.CancelAfter(10 * 1000);
try {
await Utility.InvokeProcessAsync(exe, new string[] { "--squirrel-uninstall", currentVersion.ToString() }, cts.Token).ConfigureAwait(false);
var args = new string[] { "--squirrel-uninstall", currentVersion.ToString() };
await ProcessUtil.InvokeProcessAsync(exe, args, Path.GetDirectoryName(exe), cts.Token).ConfigureAwait(false);
} catch (Exception ex) {
this.Log().ErrorException("Failed to run cleanup hook, continuing: " + exe, ex);
}
@@ -208,6 +211,7 @@ namespace Squirrel
return await createFullPackagesFromDeltas(releasesToApply.Skip(1), entry, progress).ConfigureAwait(false);
}
[SupportedOSPlatform("windows")]
void executeSelfUpdate(string newVersionDir)
{
var newSquirrel = Path.Combine(newVersionDir, "Squirrel.exe");
@@ -228,6 +232,7 @@ namespace Squirrel
Utility.Retry(() => File.Copy(newSquirrel, _config.UpdateExePath, true));
}
[SupportedOSPlatform("windows")]
async Task<string> invokePostInstall(string targetDir, bool isInitialInstall, bool firstRunOnly, bool silentInstall)
{
var versionInfo = _config.GetVersionInfoFromDirectory(targetDir);
@@ -245,7 +250,7 @@ namespace Squirrel
cts.CancelAfter(30 * 1000);
try {
await Utility.InvokeProcessAsync(exe, args, cts.Token, Path.GetDirectoryName(exe)).ConfigureAwait(false);
await ProcessUtil.InvokeProcessAsync(exe, args, Path.GetDirectoryName(exe), cts.Token).ConfigureAwait(false);
} catch (Exception ex) {
this.Log().ErrorException("Couldn't run Squirrel hook, continuing: " + exe, ex);
}
@@ -298,18 +303,20 @@ namespace Squirrel
// don't run hooks if the folder is already dead.
if (isAppFolderDead(v.DirectoryPath)) continue;
var squirrelApps = SquirrelAwareExecutableDetector.GetAllSquirrelAwareApps(v.DirectoryPath);
var args = new string[] { "--squirrel-obsolete", v.Version.ToString() };
if (SquirrelRuntimeInfo.IsWindows) {
var squirrelApps = SquirrelAwareExecutableDetector.GetAllSquirrelAwareApps(v.DirectoryPath);
var args = new string[] { "--squirrel-obsolete", v.Version.ToString() };
if (squirrelApps.Count > 0) {
// For each app, run the install command in-order and wait
foreach (var exe in squirrelApps) {
using (var cts = new CancellationTokenSource()) {
cts.CancelAfter(10 * 1000);
try {
await Utility.InvokeProcessAsync(exe, args, cts.Token).ConfigureAwait(false);
} catch (Exception ex) {
this.Log().ErrorException("Coudln't run Squirrel hook, continuing: " + exe, ex);
if (squirrelApps.Count > 0) {
// For each app, run the install command in-order and wait
foreach (var exe in squirrelApps) {
using (var cts = new CancellationTokenSource()) {
cts.CancelAfter(10 * 1000);
try {
await ProcessUtil.InvokeProcessAsync(exe, args, Path.GetDirectoryName(exe), cts.Token).ConfigureAwait(false);
} catch (Exception ex) {
this.Log().ErrorException("Coudln't run Squirrel hook, continuing: " + exe, ex);
}
}
}
}

View File

@@ -19,6 +19,12 @@ namespace Squirrel
/// <inheritdoc/>
public bool IsInstalledApp => CurrentlyInstalledVersion() != null;
/// <inheritdoc/>
public virtual SemanticVersion CurrentlyInstalledVersion() => _config.CurrentlyInstalledVersion;
/// <inheritdoc/>
public virtual string AppDirectory => _config.RootAppDir;
/// <summary>The <see cref="UpdateConfig"/> describes the structure of the application on disk (eg. file/folder locations).</summary>
public UpdateConfig Config => _config;
@@ -126,12 +132,6 @@ namespace Squirrel
await ApplyReleases(updateInfo, silentInstall, true, progress).ConfigureAwait(false);
}
/// <inheritdoc/>
public SemanticVersion CurrentlyInstalledVersion()
{
return _config.CurrentlyInstalledVersion;
}
/// <inheritdoc/>
public async Task<ReleaseEntry> UpdateApp(Action<int> progress = null)
{
@@ -173,9 +173,10 @@ namespace Squirrel
ApplyReleases(updateInfo, x => progress(x / 3 + 66)),
"Failed to apply updates").ConfigureAwait(false);
await this.ErrorIfThrows(() =>
CreateUninstallerRegistryEntry(),
"Failed to set up uninstaller").ConfigureAwait(false);
if (SquirrelRuntimeInfo.IsWindows) {
await CreateUninstallerRegistryEntry().ConfigureAwait(false);
}
} catch {
if (ignoreDeltaUpdates == false) {
ignoreDeltaUpdates = true;
@@ -259,6 +260,7 @@ namespace Squirrel
return process;
}
[SupportedOSPlatform("windows")]
private Process restartProcess(string exeToStart = null, string arguments = null)
{
// NB: Here's how this method works:
@@ -285,7 +287,7 @@ namespace Squirrel
args.Add(arguments);
}
return Process.Start(_config.UpdateExePath, Utility.ArgsToCommandLine(args));
return ProcessUtil.StartNonBlocking(_config.UpdateExePath, args, Path.GetDirectoryName(_config.UpdateExePath));
}
private static string GetLocalAppDataDirectory(string assemblyLocation = null)

View File

@@ -14,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\src\SquirrelCli\NugetConsole.cs" Link="TestHelpers\NugetConsole.cs" />
<Compile Include="..\src\Squirrel.CommandLine\NugetConsole.cs" Link="TestHelpers\NugetConsole.cs" />
</ItemGroup>
<ItemGroup>