mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Some utility code clean up
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
@@ -98,7 +100,7 @@ public class HelperExe : HelperFile
|
||||
// https://stackoverflow.com/questions/35619036/open-app-after-installation-from-pkg-file-in-mac
|
||||
var postinstall = Path.Combine(tmpScripts, "postinstall");
|
||||
File.WriteAllText(postinstall, $"#!/bin/sh\nsudo -u \"$USER\" open \"$2/{bundleName}/\"\nexit 0");
|
||||
PlatformUtil.ChmodFileAsExecutable(postinstall);
|
||||
ChmodFileAsExecutable(postinstall);
|
||||
|
||||
// generate non-relocatable component pkg. this will be included into a product archive
|
||||
var pkgPlistPath = Path.Combine(tmp, "tmp.plist");
|
||||
@@ -172,7 +174,7 @@ public class HelperExe : HelperFile
|
||||
filePath
|
||||
};
|
||||
|
||||
var ntresultjson = PlatformUtil.InvokeProcess("xcrun", args, null, CancellationToken.None);
|
||||
var ntresultjson = InvokeProcess("xcrun", args, null);
|
||||
Log.Info(ntresultjson.StdOutput);
|
||||
|
||||
// try to catch any notarization errors. if we have a submission id, retrieve notary logs.
|
||||
@@ -187,7 +189,7 @@ public class HelperExe : HelperFile
|
||||
"--keychain-profile", keychainProfileName,
|
||||
};
|
||||
|
||||
var result = PlatformUtil.InvokeProcess("xcrun", logargs, null, CancellationToken.None);
|
||||
var result = InvokeProcess("xcrun", logargs, null);
|
||||
Log.Warn(result.StdOutput);
|
||||
}
|
||||
|
||||
@@ -232,4 +234,36 @@ public class HelperExe : HelperFile
|
||||
Log.Info($"Creating ditto bundle '{outputZip}'");
|
||||
InvokeAndThrowIfNonZero("ditto", args, null);
|
||||
}
|
||||
|
||||
private const string OSX_CSTD_LIB = "libSystem.dylib";
|
||||
private const string NIX_CSTD_LIB = "libc";
|
||||
|
||||
[SupportedOSPlatform("osx")]
|
||||
[DllImport(OSX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)]
|
||||
private static extern int osx_chmod(string pathname, int mode);
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
[DllImport(NIX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)]
|
||||
private static extern int nix_chmod(string pathname, int mode);
|
||||
|
||||
protected static void ChmodFileAsExecutable(string filePath)
|
||||
{
|
||||
Func<string, int, int> chmod;
|
||||
|
||||
if (SquirrelRuntimeInfo.IsOSX) chmod = osx_chmod;
|
||||
else if (SquirrelRuntimeInfo.IsLinux) chmod = nix_chmod;
|
||||
else return; // no-op on windows, all .exe files can be executed.
|
||||
|
||||
var filePermissionOctal = Convert.ToInt32("777", 8);
|
||||
const int EINTR = 4;
|
||||
int chmodReturnCode;
|
||||
|
||||
do {
|
||||
chmodReturnCode = chmod(filePath, filePermissionOctal);
|
||||
} while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR);
|
||||
|
||||
if (chmodReturnCode == -1) {
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {filePath}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -68,7 +70,7 @@ public class HelperExe : HelperFile
|
||||
}
|
||||
|
||||
var totalToSign = pendingSign.Count;
|
||||
var baseSignArgs = PlatformUtil.CommandLineToArgvW(signArguments);
|
||||
var baseSignArgs = CommandLineToArgvW(signArguments);
|
||||
|
||||
do {
|
||||
List<string> args = new List<string>();
|
||||
@@ -78,7 +80,7 @@ public class HelperExe : HelperFile
|
||||
args.Add(pendingSign.Dequeue());
|
||||
}
|
||||
|
||||
var result = PlatformUtil.InvokeProcess(SignToolPath, args, rootDir, CancellationToken.None);
|
||||
var result = InvokeProcess(SignToolPath, args, rootDir);
|
||||
if (result.ExitCode != 0) {
|
||||
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
|
||||
Log.Debug($"Signing command failed: {cmdWithPasswordHidden}");
|
||||
@@ -101,7 +103,7 @@ public class HelperExe : HelperFile
|
||||
|
||||
var command = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\"");
|
||||
|
||||
var result = PlatformUtil.InvokeProcess(command, null, null, CancellationToken.None);
|
||||
var result = InvokeProcess(command, null, null);
|
||||
if (result.ExitCode != 0) {
|
||||
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
|
||||
Log.Debug($"Signing command failed: {cmdWithPasswordHidden}");
|
||||
@@ -144,4 +146,37 @@ public class HelperExe : HelperFile
|
||||
|
||||
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
|
||||
}
|
||||
|
||||
private const string WIN_KERNEL32 = "kernel32.dll";
|
||||
private const string WIN_SHELL32 = "shell32.dll";
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_KERNEL32, EntryPoint = "LocalFree", SetLastError = true)]
|
||||
private static extern IntPtr _LocalFree(IntPtr hMem);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_SHELL32, EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
protected static string[] CommandLineToArgvW(string cmdLine)
|
||||
{
|
||||
IntPtr argv = IntPtr.Zero;
|
||||
try {
|
||||
argv = _CommandLineToArgvW(cmdLine, out var 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
using System.Reflection;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Squirrel.Packaging;
|
||||
@@ -88,8 +92,48 @@ public class HelperFile
|
||||
|
||||
protected static string InvokeAndThrowIfNonZero(string exePath, IEnumerable<string> args, string workingDir)
|
||||
{
|
||||
var result = PlatformUtil.InvokeProcess(exePath, args, workingDir, CancellationToken.None);
|
||||
var result = InvokeProcess(exePath, args, workingDir);
|
||||
ProcessFailedException.ThrowIfNonZero(result);
|
||||
return result.StdOutput;
|
||||
}
|
||||
|
||||
protected 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());
|
||||
}
|
||||
|
||||
protected static (int ExitCode, string StdOutput, string Command) InvokeProcess(string fileName, IEnumerable<string> args, string workingDirectory, CancellationToken ct = default)
|
||||
{
|
||||
var psi = CreateProcessStartInfo(fileName, workingDirectory);
|
||||
psi.AppendArgumentListSafe(args, out var argString);
|
||||
var p = InvokeProcess(psi, ct);
|
||||
return (p.ExitCode, p.StdOutput, $"{fileName} {argString}");
|
||||
}
|
||||
|
||||
protected 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static class Disposable
|
||||
{
|
||||
public static IDisposable Create(Action action)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
||||
@@ -1,461 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
internal static class PlatformUtil
|
||||
{
|
||||
private const string OSX_CSTD_LIB = "libSystem.dylib";
|
||||
private const string NIX_CSTD_LIB = "libc";
|
||||
private const string WIN_KERNEL32 = "kernel32.dll";
|
||||
private const string WIN_SHELL32 = "shell32.dll";
|
||||
private const string WIN_NTDLL = "NTDLL.DLL";
|
||||
private const string WIN_PSAPI = "psapi.dll";
|
||||
|
||||
[SupportedOSPlatform("osx")]
|
||||
[DllImport(OSX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)]
|
||||
private static extern int osx_chmod(string pathname, int mode);
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
[DllImport(NIX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)]
|
||||
private static extern int nix_chmod(string pathname, int mode);
|
||||
|
||||
public static void ChmodFileAsExecutable(string filePath)
|
||||
{
|
||||
Func<string, int, int> chmod;
|
||||
|
||||
if (SquirrelRuntimeInfo.IsOSX) chmod = osx_chmod;
|
||||
else if (SquirrelRuntimeInfo.IsLinux) chmod = nix_chmod;
|
||||
else return; // no-op on windows, all .exe files can be executed.
|
||||
|
||||
var filePermissionOctal = Convert.ToInt32("777", 8);
|
||||
const int EINTR = 4;
|
||||
int chmodReturnCode;
|
||||
|
||||
do {
|
||||
chmodReturnCode = chmod(filePath, filePermissionOctal);
|
||||
} while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR);
|
||||
|
||||
if (chmodReturnCode == -1) {
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {filePath}.");
|
||||
}
|
||||
}
|
||||
|
||||
private enum MagicMachO : uint
|
||||
{
|
||||
MH_MAGIC = 0xfeedface,
|
||||
MH_CIGAM = 0xcefaedfe,
|
||||
MH_MAGIC_64 = 0xfeedfacf,
|
||||
MH_CIGAM_64 = 0xcffaedfe
|
||||
}
|
||||
|
||||
public static bool IsMachOImage(string filePath)
|
||||
{
|
||||
using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath))) {
|
||||
if (reader.BaseStream.Length < 256) // Header size
|
||||
return false;
|
||||
|
||||
uint magic = reader.ReadUInt32();
|
||||
return Enum.IsDefined(typeof(MagicMachO), magic);
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_PSAPI, SetLastError = true)]
|
||||
private static extern bool EnumProcesses(
|
||||
IntPtr pProcessIds, // pointer to allocated DWORD array
|
||||
int cb,
|
||||
out int pBytesReturned);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_KERNEL32, SetLastError = true)]
|
||||
private static extern bool QueryFullProcessImageName(
|
||||
IntPtr hProcess,
|
||||
[In] int justPassZeroHere,
|
||||
[Out] StringBuilder lpImageFileName,
|
||||
[In] [MarshalAs(UnmanagedType.U4)] ref int nSize);
|
||||
|
||||
[Flags]
|
||||
private enum ProcessAccess : uint
|
||||
{
|
||||
All = 0x001F0FFF,
|
||||
Terminate = 0x00000001,
|
||||
CreateThread = 0x00000002,
|
||||
VirtualMemoryOperation = 0x00000008,
|
||||
VirtualMemoryRead = 0x00000010,
|
||||
VirtualMemoryWrite = 0x00000020,
|
||||
DuplicateHandle = 0x00000040,
|
||||
CreateProcess = 0x000000080,
|
||||
SetQuota = 0x00000100,
|
||||
SetInformation = 0x00000200,
|
||||
QueryInformation = 0x00000400,
|
||||
QueryLimitedInformation = 0x00001000,
|
||||
Synchronize = 0x00100000
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_KERNEL32, SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(
|
||||
ProcessAccess processAccess,
|
||||
bool bInheritHandle,
|
||||
int processId);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_KERNEL32, SetLastError = true)]
|
||||
private static extern bool CloseHandle(IntPtr hHandle);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static List<(string ProcessExePath, int ProcessId)> GetRunningProcessesWindows()
|
||||
{
|
||||
var pids = new int[2048];
|
||||
var gch = GCHandle.Alloc(pids, GCHandleType.Pinned);
|
||||
try {
|
||||
if (!EnumProcesses(gch.AddrOfPinnedObject(), sizeof(int) * pids.Length, out var bytesReturned))
|
||||
throw new Win32Exception("Failed to enumerate processes");
|
||||
|
||||
if (bytesReturned < 1)
|
||||
throw new Exception("Failed to enumerate processes");
|
||||
|
||||
List<(string ProcessExePath, int ProcessId)> ret = new();
|
||||
|
||||
for (int i = 0; i < bytesReturned / sizeof(int); i++) {
|
||||
IntPtr hProcess = IntPtr.Zero;
|
||||
try {
|
||||
hProcess = OpenProcess(ProcessAccess.QueryLimitedInformation, false, pids[i]);
|
||||
if (hProcess == IntPtr.Zero)
|
||||
continue;
|
||||
|
||||
var sb = new StringBuilder(256);
|
||||
var capacity = sb.Capacity;
|
||||
if (!QueryFullProcessImageName(hProcess, 0, sb, ref capacity))
|
||||
continue;
|
||||
|
||||
var exePath = sb.ToString();
|
||||
if (String.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
|
||||
continue;
|
||||
|
||||
ret.Add((sb.ToString(), pids[i]));
|
||||
} catch (Exception) {
|
||||
// don't care
|
||||
} finally {
|
||||
if (hProcess != IntPtr.Zero)
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
} finally {
|
||||
gch.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<(string ProcessExePath, int ProcessId)> GetRunningProcesses()
|
||||
{
|
||||
IEnumerable<(string ProcessExePath, int ProcessId)> processes = SquirrelRuntimeInfo.IsWindows
|
||||
? GetRunningProcessesWindows()
|
||||
: Process.GetProcesses().Select(p => (p.MainModule?.FileName, p.Id));
|
||||
|
||||
return processes
|
||||
.Where(x => !String.IsNullOrWhiteSpace(x.ProcessExePath)) // Processes we can't query will have an empty process name
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<(string ProcessExePath, int ProcessId)> GetRunningProcessesInDirectory(string directory)
|
||||
{
|
||||
return GetRunningProcesses()
|
||||
.Where(x => Utility.IsFileInDirectory(x.ProcessExePath, directory))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static void KillProcessesInDirectory(ILogger logger, string directoryToKill)
|
||||
{
|
||||
logger.Info("Killing all processes in " + directoryToKill);
|
||||
var myPid = Process.GetCurrentProcess().Id;
|
||||
int c = 0;
|
||||
foreach (var x in GetRunningProcessesInDirectory(directoryToKill)) {
|
||||
if (myPid == x.ProcessId) {
|
||||
logger.Info($"Skipping '{x.ProcessExePath}' (is current process)");
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Process.GetProcessById(x.ProcessId).Kill();
|
||||
c++;
|
||||
} catch (Exception ex) {
|
||||
logger.Warn(ex, $"Unable to terminate process (pid.{x.ProcessId})");
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info($"Terminated {c} processes successfully.");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_KERNEL32, EntryPoint = "LocalFree", SetLastError = true)]
|
||||
private static extern IntPtr _LocalFree(IntPtr hMem);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_SHELL32, EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static string[] CommandLineToArgvW(string cmdLine)
|
||||
{
|
||||
IntPtr argv = IntPtr.Zero;
|
||||
try {
|
||||
argv = _CommandLineToArgvW(cmdLine, out var 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);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 (args != null) {
|
||||
#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 StartProcessNonBlocking(string fileName, IEnumerable<string> args, string workingDirectory)
|
||||
{
|
||||
var (psi, cmd) = CreateProcessStartInfo(fileName, args, workingDirectory);
|
||||
return Process.Start(psi);
|
||||
}
|
||||
|
||||
public static Process StartProcessNonBlocking(string fileName, string args, string workingDirectory)
|
||||
{
|
||||
var psi = CreateProcessStartInfo(fileName, workingDirectory);
|
||||
psi.Arguments = args;
|
||||
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));
|
||||
}
|
||||
|
||||
private enum StandardHandles : int
|
||||
{
|
||||
STD_INPUT_HANDLE = -10,
|
||||
STD_OUTPUT_HANDLE = -11,
|
||||
STD_ERROR_HANDLE = -12,
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_KERNEL32, EntryPoint = "GetStdHandle")]
|
||||
private static extern IntPtr GetStdHandle(StandardHandles nStdHandle);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_KERNEL32, EntryPoint = "AllocConsole")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_KERNEL32)]
|
||||
private static extern bool AttachConsole(int pid);
|
||||
|
||||
static int consoleCreated = 0;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static void AttachConsole()
|
||||
{
|
||||
if (Environment.OSVersion.Platform != PlatformID.Win32NT) return;
|
||||
|
||||
if (Interlocked.CompareExchange(ref consoleCreated, 1, 0) == 1) return;
|
||||
|
||||
if (!AttachConsole(-1)) {
|
||||
AllocConsole();
|
||||
}
|
||||
|
||||
GetStdHandle(StandardHandles.STD_ERROR_HANDLE);
|
||||
GetStdHandle(StandardHandles.STD_OUTPUT_HANDLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ using Squirrel.Json.Reflection;
|
||||
#endif
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
|
||||
namespace Squirrel.Json
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
@@ -83,9 +84,8 @@ namespace Squirrel.Json
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !NET5_0_OR_GREATER
|
||||
#else
|
||||
|
||||
// ReSharper disable LoopCanBeConvertedToQuery
|
||||
// ReSharper disable RedundantExplicitArrayCreation
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -15,6 +16,8 @@ namespace Squirrel
|
||||
{
|
||||
internal static class Utility
|
||||
{
|
||||
public const string SpecVersionFileName = "sq.version";
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the total percentage of a specific step that should report within a specific range.
|
||||
/// <para />
|
||||
@@ -508,6 +511,28 @@ namespace Squirrel
|
||||
return AppendPathToUri(uri, "");
|
||||
}
|
||||
|
||||
public static async Task<int> GetExitCodeAsync(this Process p)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
await p.WaitForExitAsync().ConfigureAwait(false);
|
||||
return p.ExitCode;
|
||||
#else
|
||||
var tcs = new TaskCompletionSource<int>();
|
||||
var thread = new Thread(() => {
|
||||
try {
|
||||
p.WaitForExit();
|
||||
tcs.SetResult(p.ExitCode);
|
||||
} catch (Exception ex) {
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
});
|
||||
thread.IsBackground = true;
|
||||
thread.Start();
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
return p.ExitCode;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static Uri AddQueryParamsToUri(Uri uri, IEnumerable<KeyValuePair<string, string>> newQuery)
|
||||
{
|
||||
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
|
||||
@@ -530,24 +555,6 @@ namespace Squirrel
|
||||
return peExtensions.Any(x => ext.Equals(x, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static bool IsFileTopLevelInPackage(string fullName, string pkgPath)
|
||||
{
|
||||
var fn = fullName.ToLowerInvariant();
|
||||
var pkg = pkgPath.ToLowerInvariant();
|
||||
var relativePath = fn.Replace(pkg, "");
|
||||
|
||||
// NB: We want to match things like `/lib/net45/foo.exe` but not `/lib/net45/bar/foo.exe`
|
||||
return relativePath.Split(Path.DirectorySeparatorChar).Length == 4;
|
||||
}
|
||||
|
||||
public static void ConsoleWriteWithColor(string text, ConsoleColor color)
|
||||
{
|
||||
var fc = Console.ForegroundColor;
|
||||
Console.ForegroundColor = color;
|
||||
Console.Write(text);
|
||||
Console.ForegroundColor = fc;
|
||||
}
|
||||
|
||||
public static Guid CreateGuidFromHash(string text)
|
||||
{
|
||||
return CreateGuidFromHash(text, Utility.IsoOidNamespace);
|
||||
@@ -629,32 +636,6 @@ namespace Squirrel
|
||||
guid[right] = temp;
|
||||
}
|
||||
|
||||
public const string SpecVersionFileName = "sq.version";
|
||||
|
||||
public static NuspecManifest ReadManifestFromVersionDir(string appVersionDir)
|
||||
{
|
||||
NuspecManifest manifest;
|
||||
string nuspec;
|
||||
|
||||
nuspec = Path.Combine(appVersionDir, SpecVersionFileName);
|
||||
if (File.Exists(nuspec) && NuspecManifest.TryParseFromFile(nuspec, out manifest))
|
||||
return manifest;
|
||||
|
||||
nuspec = Path.Combine(appVersionDir, "Contents", SpecVersionFileName);
|
||||
if (File.Exists(nuspec) && NuspecManifest.TryParseFromFile(nuspec, out manifest))
|
||||
return manifest;
|
||||
|
||||
nuspec = Path.Combine(appVersionDir, "mysqver");
|
||||
if (File.Exists(nuspec) && NuspecManifest.TryParseFromFile(nuspec, out manifest))
|
||||
return manifest;
|
||||
|
||||
nuspec = Path.Combine(appVersionDir, "current.version");
|
||||
if (File.Exists(nuspec) && NuspecManifest.TryParseFromFile(nuspec, out manifest))
|
||||
return manifest;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void CopyFiles(string source, string target)
|
||||
{
|
||||
CopyFiles(new DirectoryInfo(source), new DirectoryInfo(target));
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Squirrel.Locators
|
||||
var appPath = ourPath.Substring(0, ix + 4);
|
||||
var contentsDir = Path.Combine(appPath, "Contents");
|
||||
var updateExe = Path.Combine(contentsDir, "UpdateMac");
|
||||
var metadataPath = Path.Combine(contentsDir, "sq.version");
|
||||
var metadataPath = Path.Combine(contentsDir, Utility.SpecVersionFileName);
|
||||
|
||||
if (File.Exists(updateExe) && NuspecManifest.TryParseFromFile(metadataPath, out var manifest)) {
|
||||
Log.Info("Located valid manifest file at: " + metadataPath);
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Squirrel.Locators
|
||||
if (File.Exists(possibleUpdateExe)) {
|
||||
Log.Info("Update.exe found in parent directory");
|
||||
// we're running in a directory with an Update.exe in the parent directory
|
||||
var manifestFile = Path.Combine(myDirPath, "sq.version");
|
||||
var manifestFile = Path.Combine(myDirPath, Utility.SpecVersionFileName);
|
||||
if (NuspecManifest.TryParseFromFile(manifestFile, out var manifest)) {
|
||||
// ideal, the info we need is in a manifest file.
|
||||
Log.Info("Located valid manifest file at: " + manifestFile);
|
||||
@@ -88,7 +88,7 @@ namespace Squirrel.Locators
|
||||
// this is an attempt to handle the case where we are running in a nested current directory.
|
||||
var rootDir = ourExePath.Substring(0, ixCurrent);
|
||||
var currentDir = Path.Combine(rootDir, "current");
|
||||
var manifestFile = Path.Combine(currentDir, "sq.version");
|
||||
var manifestFile = Path.Combine(currentDir, Utility.SpecVersionFileName);
|
||||
possibleUpdateExe = Path.GetFullPath(Path.Combine(rootDir, "Update.exe"));
|
||||
// we only support parsing a manifest when we're in a nested current directory. no legacy fallback.
|
||||
if (File.Exists(possibleUpdateExe) && NuspecManifest.TryParseFromFile(manifestFile, out var manifest)) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
@@ -87,17 +88,21 @@ namespace Squirrel.Windows
|
||||
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 PlatformUtil.InvokeProcessAsync(pathToInstaller, isQuiet ? quietArgs : args, null, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var psi = new ProcessStartInfo(pathToInstaller);
|
||||
psi.AppendArgumentListSafe(isQuiet ? quietArgs : args, out var _);
|
||||
var p = Process.Start(psi);
|
||||
var code = await p.GetExitCodeAsync().ConfigureAwait(false);
|
||||
|
||||
// https://johnkoerner.com/install/windows-installer-error-codes/
|
||||
|
||||
if (p.ExitCode == 1638) // a newer compatible version is already installed
|
||||
if (code == 1638) // a newer compatible version is already installed
|
||||
return RuntimeInstallResult.InstallSuccess;
|
||||
|
||||
if (p.ExitCode == 1641) // installer initiated a restart
|
||||
if (code == 1641) // installer initiated a restart
|
||||
return RuntimeInstallResult.RestartRequired;
|
||||
|
||||
return (RuntimeInstallResult) p.ExitCode;
|
||||
return (RuntimeInstallResult) code;
|
||||
}
|
||||
|
||||
/// <summary> The unique string representation of this runtime </summary>
|
||||
|
||||
@@ -174,25 +174,6 @@ namespace Squirrel.Tests
|
||||
Assert.Equal(result, Utility.FileIsLikelyPEImage(input));
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[InlineData("C:\\Users\\bob\\temp\\pkgPath\\lib\\net45\\foo.exe", "C:\\Users\\bob\\temp\\pkgPath", true)]
|
||||
[InlineData("C:\\Users\\bob\\temp\\pkgPath\\lib\\net45\\node_modules\\foo.exe", "C:\\Users\\bob\\temp\\pkgPath", false)]
|
||||
[InlineData("C:\\Users\\bob\\temp\\pkgPath\\lib\\net45\\node_modules\\foo\\foo.exe", "C:\\Users\\bob\\temp\\pkgPath", false)]
|
||||
[InlineData("foo.png", "C:\\Users\\bob\\temp\\pkgPath", false)]
|
||||
public void IsFileTopLevelInPackageTest(string input, string packagePath, bool result)
|
||||
{
|
||||
Skip.IfNot(SquirrelRuntimeInfo.IsWindows);
|
||||
Assert.Equal(result, Utility.IsFileTopLevelInPackage(input, packagePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WeCanFetchAllProcesses()
|
||||
{
|
||||
var result = PlatformUtil.GetRunningProcesses();
|
||||
Assert.True(result.Count > 1);
|
||||
Assert.True(result.Count != 2048);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Only really need to run this test after changes to FileDownloader")]
|
||||
public async Task DownloaderReportsProgress()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user