diff --git a/src/Rust/src/bundle.rs b/src/Rust/src/bundle.rs index af88cb02..456cf907 100644 --- a/src/Rust/src/bundle.rs +++ b/src/Rust/src/bundle.rs @@ -139,9 +139,9 @@ impl BundleInfo<'_> { None } - pub fn extract_zip_idx_to_path>(&self, index: usize, path: T) -> Result<()> { + pub fn extract_zip_idx_to_path>(&self, index: usize, path: T) -> Result<()> { let path = path.as_ref(); - debug!("Extracting zip file to path: {}", path); + debug!("Extracting zip file to path: {}", path.to_string_lossy()); let p = PathBuf::from(path); let parent = p.parent().unwrap(); @@ -160,7 +160,7 @@ impl BundleInfo<'_> { Ok(()) } - pub fn extract_zip_predicate_to_path>(&self, predicate: F, path: T) -> Result + pub fn extract_zip_predicate_to_path>(&self, predicate: F, path: T) -> Result where F: Fn(&str) -> bool, { @@ -183,6 +183,11 @@ impl BundleInfo<'_> { let stub_regex = Regex::new("_ExecutionStub.exe$").unwrap(); let updater_idx = self.find_zip_file(|name| name.ends_with("Squirrel.exe")); + let nuspec_path = current_path.join("sq.version"); + let _ = self + .extract_zip_predicate_to_path(|name| name.ends_with(".nuspec"), nuspec_path) + .map_err(|_| anyhow!("This package is missing a nuspec manifest."))?; + for (i, key) in files.iter().enumerate() { if Some(i) == updater_idx || !re.is_match(key) || key.ends_with("/") || key.ends_with("\\") { info!(" {} Skipped '{}'", i, key); diff --git a/src/Rust/src/update.rs b/src/Rust/src/update.rs index 0873a4fa..15a0c327 100644 --- a/src/Rust/src/update.rs +++ b/src/Rust/src/update.rs @@ -185,6 +185,10 @@ fn apply<'a>(matches: &ArgMatches) -> Result<()> { info!(" Exe Name: {:?}", exe_name); info!(" Exe Args: {:?}", exe_args); + if wait_for_parent { + let _ = platform::wait_for_parent_to_exit(60_000); // 1 minute + } + if let Err(e) = apply_package(package) { error!("Error applying package: {}", e); if !restart { @@ -193,7 +197,7 @@ fn apply<'a>(matches: &ArgMatches) -> Result<()> { } if restart { - _start(wait_for_parent, exe_name, exe_args, None)?; + _start(false, exe_name, exe_args, None)?; } Ok(()) @@ -240,6 +244,8 @@ fn apply_package<'a>(package: Option<&PathBuf>) -> Result<()> { bail!("Latest package found is {}, which is not newer than current version {}.", found_version, app.version); } + info!("Applying package to current: {}", found_version); + let current_dir = app.get_current_path(&root_path); replace_dir_with_rollback(current_dir.clone(), || { if let Some(bundle) = package_bundle.take() { diff --git a/src/Squirrel/Internal/ProcessArgumentListPolyfill.cs b/src/Squirrel/Internal/ProcessArgumentListPolyfill.cs new file mode 100644 index 00000000..68d815f5 --- /dev/null +++ b/src/Squirrel/Internal/ProcessArgumentListPolyfill.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace Squirrel +{ + internal static class ProcessArgumentListPolyfill + { + +#if NET5_0_OR_GREATER + public static void AppendArgumentListSafe(this ProcessStartInfo psi, IEnumerable args, out string debug) + { + foreach (var a in args) { + psi.ArgumentList.Add(a); + } + var sb = new StringBuilder(); + AppendArgumentsTo(sb, args); + debug = sb.ToString(); + } + +#else + public static void AppendArgumentListSafe(this ProcessStartInfo psi, IEnumerable args, out string debug) + { + var sb = new StringBuilder(); + AppendArgumentsTo(sb, args); + psi.Arguments = sb.ToString(); + debug = psi.Arguments; + } +#endif + // https://source.dot.net/#System.Diagnostics.Process/System/Diagnostics/ProcessStartInfo.cs,204 + private static void AppendArgumentsTo(StringBuilder stringBuilder, IEnumerable args) + { + if (args != null && args.Any()) { + foreach (string argument in args) { + AppendArgument(stringBuilder, argument); + } + } + } + + // https://source.dot.net/#System.Diagnostics.Process/src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs,624678ba1465e776 + private static void AppendArgument(StringBuilder stringBuilder, string argument) + { + if (stringBuilder.Length != 0) { + stringBuilder.Append(' '); + } + + // Parsing rules for non-argv[0] arguments: + // - Backslash is a normal character except followed by a quote. + // - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote + // - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote + // - Parsing stops at first whitespace outside of quoted region. + // - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode. + if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument)) { + // Simple case - no quoting or changes needed. + stringBuilder.Append(argument); + } else { + stringBuilder.Append(Quote); + int idx = 0; + while (idx < argument.Length) { + char c = argument[idx++]; + if (c == Backslash) { + int numBackSlash = 1; + while (idx < argument.Length && argument[idx] == Backslash) { + idx++; + numBackSlash++; + } + + if (idx == argument.Length) { + // We'll emit an end quote after this so must double the number of backslashes. + stringBuilder.Append(Backslash, numBackSlash * 2); + } else if (argument[idx] == Quote) { + // Backslashes will be followed by a quote. Must double the number of backslashes. + stringBuilder.Append(Backslash, numBackSlash * 2 + 1); + stringBuilder.Append(Quote); + idx++; + } else { + // Backslash will not be followed by a quote, so emit as normal characters. + stringBuilder.Append(Backslash, numBackSlash); + } + + continue; + } + + if (c == Quote) { + // Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed + // by another quote (which parses differently pre-2008 vs. post-2008.) + stringBuilder.Append(Backslash); + stringBuilder.Append(Quote); + continue; + } + + stringBuilder.Append(c); + } + + stringBuilder.Append(Quote); + } + } + + private static bool ContainsNoWhitespaceOrQuotes(string s) + { + for (int i = 0; i < s.Length; i++) { + char c = s[i]; + if (char.IsWhiteSpace(c) || c == Quote) { + return false; + } + } + + return true; + } + + private const char Quote = '\"'; + private const char Backslash = '\\'; + } +} diff --git a/src/Squirrel/UpdateManager.cs b/src/Squirrel/UpdateManager.cs index 5c4c5064..88fc8601 100644 --- a/src/Squirrel/UpdateManager.cs +++ b/src/Squirrel/UpdateManager.cs @@ -215,12 +215,7 @@ namespace Squirrel WorkingDirectory = Path.GetDirectoryName(Locator.UpdateExePath), }; -#if NET5_0_OR_GREATER - var args = psi.ArgumentList; -#else var args = new List(); -#endif - if (silent) args.Add("--silent"); args.Add("apply"); args.Add("--wait"); @@ -239,9 +234,8 @@ namespace Squirrel } } -#if !NET5_0_OR_GREATER - psi.Arguments = String.Join(" ", args); -#endif + psi.AppendArgumentListSafe(args, out var debugArgs); + Log.Debug($"Restarting app to apply updates. Running: {psi.FileName} {debugArgs}"); var p = Process.Start(psi); Thread.Sleep(300); @@ -251,6 +245,7 @@ namespace Squirrel if (p.HasExited) { throw new Exception($"Update.exe process exited too soon ({p.ExitCode})."); } + Log.Info("Update.exe apply triggered successfully."); } protected virtual async Task DownloadAndApplyDeltaUpdates(string extractedBasePackage, UpdateInfo updates, Action progress) diff --git a/test/Squirrel.Packaging.Tests/WindowsPackTests.cs b/test/Squirrel.Packaging.Tests/WindowsPackTests.cs index 717d6e2c..e11e3400 100644 --- a/test/Squirrel.Packaging.Tests/WindowsPackTests.cs +++ b/test/Squirrel.Packaging.Tests/WindowsPackTests.cs @@ -280,10 +280,11 @@ public class WindowsPackTests // check app installed correctly var appPath = Path.Combine(installDir, "current", "TestApp.exe"); Assert.True(File.Exists(appPath)); - var argsPath = Path.Combine(installDir, "current", "args.txt"); + var argsPath = Path.Combine(installDir, "args.txt"); Assert.True(File.Exists(argsPath)); var argsContent = File.ReadAllText(argsPath).Trim(); Assert.Equal("--squirrel-install 1.0.0", argsContent); + logger.Info("TEST: v1 installed"); // check app output var chk1test = RunProcess(appPath, new string[] { "test" }, installDir, logger); @@ -292,6 +293,7 @@ public class WindowsPackTests Assert.EndsWith(Environment.NewLine + "1.0.0", chk1version); var chk1check = RunProcess(appPath, new string[] { "check", releaseDir }, installDir, logger); Assert.EndsWith(Environment.NewLine + "no updates", chk1check); + logger.Info("TEST: v1 output verified"); // pack v2 PackTestApp("2.0.0", "version 2 test", releaseDir, logger); @@ -299,15 +301,17 @@ public class WindowsPackTests // check can find v2 update var chk2check = RunProcess(appPath, new string[] { "check", releaseDir }, installDir, logger); Assert.EndsWith(Environment.NewLine + "update: 2.0.0", chk2check); + logger.Info("TEST: found v2 update"); // pack v3 PackTestApp("3.0.0", "version 3 test", releaseDir, logger); // perform full update, check that we get v3 // apply should fail if there's not an update downloaded - RunProcess(appPath, new string[] { "apply", releaseDir }, installDir, logger, -1); + RunProcess(appPath, new string[] { "apply", releaseDir }, installDir, logger, exitCode: -1); RunProcess(appPath, new string[] { "download", releaseDir }, installDir, logger); - RunProcess(appPath, new string[] { "apply", releaseDir }, installDir, logger); + RunProcess(appPath, new string[] { "apply", releaseDir }, installDir, logger, exitCode: null); + logger.Info("TEST: v3 applied"); // check app output var chk3test = RunProcess(appPath, new string[] { "test" }, installDir, logger); @@ -316,12 +320,15 @@ public class WindowsPackTests Assert.EndsWith(Environment.NewLine + "3.0.0", chk3version); var ch3check2 = RunProcess(appPath, new string[] { "check", releaseDir }, installDir, logger); Assert.EndsWith(Environment.NewLine + "no updates", ch3check2); + logger.Info("TEST: v3 output verified"); + // check new obsoleted/updated hooks have run var argsContentv3 = File.ReadAllText(argsPath).Trim(); Assert.Contains("--squirrel-install 1.0.0", argsContent); Assert.Contains("--squirrel-obsoleted 1.0.0", argsContent); Assert.Contains("--squirrel-updated 3.0.0", argsContent); + logger.Info("TEST: hooks verified"); @@ -339,11 +346,12 @@ public class WindowsPackTests // uninstall var updatePath = Path.Combine(installDir, "Update.exe"); RunProcess(updatePath, new string[] { "--nocolor", "--silent", "--uninstall" }, Environment.CurrentDirectory, logger); + logger.Info("TEST: uninstalled / complete"); } const string TEST_APP_ID = "Test.Squirrel-App"; - private string RunProcess(string exe, string[] args, string workingDir, ILogger logger, int exitCode = 0) + private string RunProcess(string exe, string[] args, string workingDir, ILogger logger, int? exitCode = 0) { var psi = new ProcessStartInfo(exe); psi.WorkingDirectory = workingDir; @@ -360,7 +368,9 @@ public class WindowsPackTests p.ErrorDataReceived += (s, e) => { sb.AppendLine(e.Data); logger.Debug(e.Data); }; p.WaitForExit(); - Assert.Equal(exitCode, p.ExitCode); + if (exitCode != null) + Assert.Equal(exitCode, p.ExitCode); + return sb.ToString().Trim(); } diff --git a/test/TestApp/Program.cs b/test/TestApp/Program.cs index 990372f8..e7285d77 100644 --- a/test/TestApp/Program.cs +++ b/test/TestApp/Program.cs @@ -4,18 +4,18 @@ using Squirrel.Locators; try { if (args.Length >= 1 && args[0].StartsWith("--squirrel")) { // squirrel hooks - File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "args.txt"), String.Join(" ", args) + Environment.NewLine); + File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "..", "args.txt"), String.Join(" ", args) + Environment.NewLine); return 0; } if (args.Length == 1 && args[0] == "version") { var locator = SquirrelLocator.GetDefault(new ConsoleLogger()); - Console.WriteLine(locator.CurrentlyInstalledVersion); + Console.WriteLine(locator.CurrentlyInstalledVersion?.ToString() ?? "unknown_version"); return 0; } if (args.Length == 1 && args[0] == "test") { - Console.WriteLine(Const.TEST_STRING); + Console.WriteLine(Const.TEST_STRING ?? "no_test_string"); return 0; }