From ad4fdb420854a242508eb260b26052aa0455e44e Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Tue, 26 Dec 2023 13:28:55 +0000 Subject: [PATCH] Some utility code clean up --- src/Squirrel.Packaging.OSX/HelperExe.cs | 42 +- src/Squirrel.Packaging.Windows/HelperExe.cs | 43 +- src/Squirrel.Packaging/HelperFile.cs | 48 +- src/Squirrel/Compression/BZip2Stream.cs | 3 +- .../Compression/BinaryPatchUtility.cs | 3 +- src/Squirrel/Internal/Disposable.cs | 2 + src/Squirrel/Internal/LoggerExtensions.cs | 3 +- src/Squirrel/Internal/PlatformUtil.cs | 461 ------------------ src/Squirrel/Internal/SimpleJson.cs | 4 +- src/Squirrel/Internal/Utility.cs | 69 +-- src/Squirrel/Locators/OsxSquirrelLocator.cs | 2 +- .../Locators/WindowsSquirrelLocator.cs | 4 +- src/Squirrel/Windows/RuntimeInfo.cs | 13 +- test/Squirrel.Tests/UtilityTests.cs | 19 - 14 files changed, 167 insertions(+), 549 deletions(-) delete mode 100644 src/Squirrel/Internal/PlatformUtil.cs diff --git a/src/Squirrel.Packaging.OSX/HelperExe.cs b/src/Squirrel.Packaging.OSX/HelperExe.cs index 87323937a..321391ba7 100644 --- a/src/Squirrel.Packaging.OSX/HelperExe.cs +++ b/src/Squirrel.Packaging.OSX/HelperExe.cs @@ -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 void CreateInstallerPkg(string appBundlePath, string appTitle, IEnumerabl // 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 void Notarize(string filePath, string keychainProfileName) 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 void Notarize(string filePath, string keychainProfileName) "--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 void CreateDittoZip(string folder, string outputZip) 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 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}."); + } + } } \ No newline at end of file diff --git a/src/Squirrel.Packaging.Windows/HelperExe.cs b/src/Squirrel.Packaging.Windows/HelperExe.cs index 431309077..692ea6974 100644 --- a/src/Squirrel.Packaging.Windows/HelperExe.cs +++ b/src/Squirrel.Packaging.Windows/HelperExe.cs @@ -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 void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string s } var totalToSign = pendingSign.Count; - var baseSignArgs = PlatformUtil.CommandLineToArgvW(signArguments); + var baseSignArgs = CommandLineToArgvW(signArguments); do { List args = new List(); @@ -78,7 +80,7 @@ public void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string s 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 void SignPEFileWithTemplate(string filePath, string signTemplate) 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 void SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPackage pack 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); + } + } } \ No newline at end of file diff --git a/src/Squirrel.Packaging/HelperFile.cs b/src/Squirrel.Packaging/HelperFile.cs index f7f6aac42..b68651dd6 100644 --- a/src/Squirrel.Packaging/HelperFile.cs +++ b/src/Squirrel.Packaging/HelperFile.cs @@ -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 @@ protected static string FindHelperFile(string toFind, Func predica protected static string InvokeAndThrowIfNonZero(string exePath, IEnumerable 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 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; + } } diff --git a/src/Squirrel/Compression/BZip2Stream.cs b/src/Squirrel/Compression/BZip2Stream.cs index c0d303932..0da7ca337 100644 --- a/src/Squirrel/Compression/BZip2Stream.cs +++ b/src/Squirrel/Compression/BZip2Stream.cs @@ -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; diff --git a/src/Squirrel/Compression/BinaryPatchUtility.cs b/src/Squirrel/Compression/BinaryPatchUtility.cs index f8d2294ad..2c9db95e3 100644 --- a/src/Squirrel/Compression/BinaryPatchUtility.cs +++ b/src/Squirrel/Compression/BinaryPatchUtility.cs @@ -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; diff --git a/src/Squirrel/Internal/Disposable.cs b/src/Squirrel/Internal/Disposable.cs index 886cbb675..284dd3673 100644 --- a/src/Squirrel/Internal/Disposable.cs +++ b/src/Squirrel/Internal/Disposable.cs @@ -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) diff --git a/src/Squirrel/Internal/LoggerExtensions.cs b/src/Squirrel/Internal/LoggerExtensions.cs index 35576db3c..73ec7db0c 100644 --- a/src/Squirrel/Internal/LoggerExtensions.cs +++ b/src/Squirrel/Internal/LoggerExtensions.cs @@ -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; diff --git a/src/Squirrel/Internal/PlatformUtil.cs b/src/Squirrel/Internal/PlatformUtil.cs deleted file mode 100644 index 6b8782f2e..000000000 --- a/src/Squirrel/Internal/PlatformUtil.cs +++ /dev/null @@ -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 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 - */ - - /// - /// 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. - /// - /// 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 . - [SupportedOSPlatform("windows")] - private static string ArgsToCommandLine(IEnumerable 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' }; - - /// - /// Escapes all cmd.exe meta-characters by prefixing them with a ^. See for more - /// information. - [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 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 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 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 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 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); - } - } -} \ No newline at end of file diff --git a/src/Squirrel/Internal/SimpleJson.cs b/src/Squirrel/Internal/SimpleJson.cs index 07523849e..9bdd39301 100644 --- a/src/Squirrel/Internal/SimpleJson.cs +++ b/src/Squirrel/Internal/SimpleJson.cs @@ -72,6 +72,7 @@ #endif #if NET5_0_OR_GREATER + namespace Squirrel.Json { [ExcludeFromCodeCoverage] @@ -83,9 +84,8 @@ public static T DeserializeObject(string json) } } } -#endif -#if !NET5_0_OR_GREATER +#else // ReSharper disable LoopCanBeConvertedToQuery // ReSharper disable RedundantExplicitArrayCreation diff --git a/src/Squirrel/Internal/Utility.cs b/src/Squirrel/Internal/Utility.cs index cdcf8e3ac..98a139c97 100644 --- a/src/Squirrel/Internal/Utility.cs +++ b/src/Squirrel/Internal/Utility.cs @@ -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"; + /// /// Calculates the total percentage of a specific step that should report within a specific range. /// @@ -508,6 +511,28 @@ public static Uri EnsureTrailingSlash(Uri uri) return AppendPathToUri(uri, ""); } + public static async Task GetExitCodeAsync(this Process p) + { +#if NET5_0_OR_GREATER + await p.WaitForExitAsync().ConfigureAwait(false); + return p.ExitCode; +#else + var tcs = new TaskCompletionSource(); + 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> newQuery) { var query = System.Web.HttpUtility.ParseQueryString(uri.Query); @@ -530,24 +555,6 @@ public static bool FileIsLikelyPEImage(string name) 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 @@ static void SwapBytes(byte[] guid, int left, int right) 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)); diff --git a/src/Squirrel/Locators/OsxSquirrelLocator.cs b/src/Squirrel/Locators/OsxSquirrelLocator.cs index 44c6e2050..78ebe71ad 100644 --- a/src/Squirrel/Locators/OsxSquirrelLocator.cs +++ b/src/Squirrel/Locators/OsxSquirrelLocator.cs @@ -58,7 +58,7 @@ public OsxSquirrelLocator(ILogger logger) 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); diff --git a/src/Squirrel/Locators/WindowsSquirrelLocator.cs b/src/Squirrel/Locators/WindowsSquirrelLocator.cs index ce59b79e2..5dc86a7d6 100644 --- a/src/Squirrel/Locators/WindowsSquirrelLocator.cs +++ b/src/Squirrel/Locators/WindowsSquirrelLocator.cs @@ -66,7 +66,7 @@ internal WindowsSquirrelLocator(string ourExePath, ILogger logger) 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 @@ internal WindowsSquirrelLocator(string ourExePath, ILogger logger) // 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)) { diff --git a/src/Squirrel/Windows/RuntimeInfo.cs b/src/Squirrel/Windows/RuntimeInfo.cs index 1e08f8387..fea9cac7e 100644 --- a/src/Squirrel/Windows/RuntimeInfo.cs +++ b/src/Squirrel/Windows/RuntimeInfo.cs @@ -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 @@ public virtual async Task InvokeInstaller(string pathToIns 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; } /// The unique string representation of this runtime diff --git a/test/Squirrel.Tests/UtilityTests.cs b/test/Squirrel.Tests/UtilityTests.cs index 94305134c..0cba3403f 100644 --- a/test/Squirrel.Tests/UtilityTests.cs +++ b/test/Squirrel.Tests/UtilityTests.cs @@ -174,25 +174,6 @@ public void FileIsLikelyPEImageTest(string input, bool result) 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() {