diff --git a/Squirrel.sln b/Squirrel.sln index 0a17c88c..e38b4641 100644 --- a/Squirrel.sln +++ b/Squirrel.sln @@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Deployment", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Packaging.Tests", "test\Squirrel.Packaging.Tests\Squirrel.Packaging.Tests.csproj", "{175B06A5-5C09-4DAB-A6AF-C8A2257BD1B6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "test\TestApp\TestApp.csproj", "{784B5987-2E71-4AEE-81B9-E0CC7F1DBEB3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -74,6 +76,10 @@ Global {175B06A5-5C09-4DAB-A6AF-C8A2257BD1B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {175B06A5-5C09-4DAB-A6AF-C8A2257BD1B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {175B06A5-5C09-4DAB-A6AF-C8A2257BD1B6}.Release|Any CPU.Build.0 = Release|Any CPU + {784B5987-2E71-4AEE-81B9-E0CC7F1DBEB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {784B5987-2E71-4AEE-81B9-E0CC7F1DBEB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {784B5987-2E71-4AEE-81B9-E0CC7F1DBEB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {784B5987-2E71-4AEE-81B9-E0CC7F1DBEB3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -82,6 +88,7 @@ Global {98AEB048-E27D-42F4-9440-505B7F78BAFD} = {7AC3A776-B582-4B65-9D03-BD52332B5CA3} {519EAB50-47B8-425F-8B20-AB9548F220B4} = {7AC3A776-B582-4B65-9D03-BD52332B5CA3} {175B06A5-5C09-4DAB-A6AF-C8A2257BD1B6} = {7AC3A776-B582-4B65-9D03-BD52332B5CA3} + {784B5987-2E71-4AEE-81B9-E0CC7F1DBEB3} = {7AC3A776-B582-4B65-9D03-BD52332B5CA3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {68CA987A-9BAB-4C75-8EEB-4596BA6BBD07} diff --git a/src/Rust/src/update.rs b/src/Rust/src/update.rs index dde6fa36..d97fc973 100644 --- a/src/Rust/src/update.rs +++ b/src/Rust/src/update.rs @@ -28,7 +28,9 @@ fn root_command() -> Command { .about("Applies a staged / prepared update, installing prerequisite runtimes if necessary") .arg(arg!(-r --restart "Restart the application after the update")) .arg(arg!(-w --wait "Wait for the parent process to terminate before applying the update")) - // .arg(arg!(-p --pkg "Update package to apply").value_parser(value_parser!(PathBuf))) + .arg(arg!(-p --package "Update package to apply").value_parser(value_parser!(PathBuf))) + .arg(arg!([EXE_NAME] "The optional name of the binary to execute")) + .arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceeded by '--'.").required(false).last(true).num_args(0..)) ) .subcommand(Command::new("start") .about("Starts the currently installed version of the application") diff --git a/src/Squirrel/Internal/Utility.cs b/src/Squirrel/Internal/Utility.cs index 87af4fab..753092f4 100644 --- a/src/Squirrel/Internal/Utility.cs +++ b/src/Squirrel/Internal/Utility.cs @@ -287,7 +287,7 @@ namespace Squirrel private static string GetNextTempName(string tempDir) { - for (int i = 1; i < 10000; i++) { + for (int i = 1; i < 1000; i++) { string name = "temp." + i; var target = Path.Combine(tempDir, name); @@ -311,7 +311,7 @@ namespace Squirrel } throw new Exception( - "Unable to find free temp path. Has the temp directory exceeded it's maximum number of items? (10000)"); + "Unable to find free temp path. Has the temp directory exceeded it's maximum number of items? (1000)"); } public static IDisposable GetTempDirectory(out string newTempDirectory) diff --git a/src/Squirrel/Locators/ISquirrelLocator.cs b/src/Squirrel/Locators/ISquirrelLocator.cs index 35563f3f..a3d36846 100644 --- a/src/Squirrel/Locators/ISquirrelLocator.cs +++ b/src/Squirrel/Locators/ISquirrelLocator.cs @@ -33,6 +33,9 @@ namespace Squirrel.Locators /// The currently installed version of the application, or null if the app is not installed. public SemanticVersion CurrentlyInstalledVersion { get; } + /// The path from to this executable. + public string ThisExeRelativePath { get; } + /// /// Finds .nupkg files in the PackagesDir and returns a list of ReleaseEntryName objects. /// diff --git a/src/Squirrel/Locators/SquirrelLocator.cs b/src/Squirrel/Locators/SquirrelLocator.cs index bff52bf8..449c52fc 100644 --- a/src/Squirrel/Locators/SquirrelLocator.cs +++ b/src/Squirrel/Locators/SquirrelLocator.cs @@ -21,6 +21,9 @@ namespace Squirrel.Locators /// public static SquirrelLocator GetDefault(ILogger logger) { + if (_current != null) + return _current; + if (SquirrelRuntimeInfo.IsWindows) return _current ??= new WindowsSquirrelLocator(logger); @@ -48,6 +51,18 @@ namespace Squirrel.Locators /// public abstract string AppContentDir { get; } + /// + public virtual string ThisExeRelativePath { + get { + var path = SquirrelRuntimeInfo.EntryExePath; + if (path.StartsWith(AppContentDir, StringComparison.OrdinalIgnoreCase)) { + return path.Substring(AppContentDir.Length + 1); + } else { + throw new InvalidOperationException(path + " is not contained in " + AppContentDir); + } + } + } + /// public abstract SemanticVersion CurrentlyInstalledVersion { get; } diff --git a/src/Squirrel/UpdateManager.cs b/src/Squirrel/UpdateManager.cs index f07b5bc2..5c4c5064 100644 --- a/src/Squirrel/UpdateManager.cs +++ b/src/Squirrel/UpdateManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -21,7 +22,7 @@ namespace Squirrel public virtual bool IsInstalled => Locator.CurrentlyInstalledVersion != null; - public virtual bool IsPendingRestart { + public virtual bool IsUpdatePendingRestart { get { var latestLocal = Locator.GetLatestLocalPackage(); if (latestLocal != null && latestLocal.Version > CurrentVersion) @@ -38,21 +39,11 @@ namespace Squirrel protected ISquirrelLocator Locator { get; } - public UpdateManager(string urlOrPath, string channel = null, ILogger logger = null) - : this(urlOrPath, channel, logger, null) - { - } - public UpdateManager(string urlOrPath, string channel = null, ILogger logger = null, ISquirrelLocator locator = null) : this(CreateSimpleSource(urlOrPath, channel, logger), logger, locator) { } - public UpdateManager(IUpdateSource source, ILogger logger = null) - : this(source, logger, null) - { - } - public UpdateManager(IUpdateSource source, ILogger logger = null, ISquirrelLocator locator = null) { if (source == null) { @@ -117,13 +108,13 @@ namespace Squirrel return new UpdateInfo(latestRemoteFull, latestLocalFull, deltas); } - public void DownloadAndPrepareUpdates(UpdateInfo updates, Action progress = null, bool ignoreDeltas = false) + public void DownloadUpdates(UpdateInfo updates, Action progress = null, bool ignoreDeltas = false) { - DownloadAndPrepareUpdatesAsync(updates, progress, ignoreDeltas) + DownloadUpdatesAsync(updates, progress, ignoreDeltas) .ConfigureAwait(false).GetAwaiter().GetResult(); } - public virtual async Task DownloadAndPrepareUpdatesAsync( + public virtual async Task DownloadUpdatesAsync( UpdateInfo updates, Action progress = null, bool ignoreDeltas = false, CancellationToken cancelToken = default) { try { @@ -204,14 +195,62 @@ namespace Squirrel } } - public void ExitAndApplyUpdates() + public void ApplyUpdatesAndExit(bool silent = false) { - + RunApplyUpdates(silent, false, null); + Environment.Exit(0); } - public void WaitForExitAndApplyUpdates(bool restart, string[] arguments = null) + public void ApplyUpdatesAndRestart(bool silent = false, string[] restartArgs = null) { - var updateArgs = new string[]{ "--update" }; + RunApplyUpdates(silent, true, restartArgs); + Environment.Exit(0); + } + + protected virtual void RunApplyUpdates(bool silent, bool restart, string[] restartArgs) + { + var psi = new ProcessStartInfo() { + CreateNoWindow = true, + FileName = Locator.UpdateExePath, + 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"); + if (restart) args.Add("--restart"); + + try { + args.Add(Locator.ThisExeRelativePath); // optional + } catch (Exception ex) { + Log.Error(ex, "Failed to find relative path to this executable."); + } + + if (restart && restartArgs != null && restartArgs.Length > 0) { + args.Add("--"); + foreach (var a in restartArgs) { + args.Add(a); + } + } + +#if !NET5_0_OR_GREATER + psi.Arguments = String.Join(" ", args); +#endif + + var p = Process.Start(psi); + Thread.Sleep(300); + if (p == null) { + throw new Exception("Failed to launch Update.exe process."); + } + if (p.HasExited) { + throw new Exception($"Update.exe process exited too soon ({p.ExitCode})."); + } } 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 0e19288b..717d6e2c 100644 --- a/test/Squirrel.Packaging.Tests/WindowsPackTests.cs +++ b/test/Squirrel.Packaging.Tests/WindowsPackTests.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Runtime.Versioning; using System.Text; using System.Xml.Linq; +using Microsoft.Extensions.Logging; using Microsoft.Win32; using NuGet.Packaging; using Squirrel.Compression; @@ -223,9 +224,7 @@ public class WindowsPackTests var setupPath1 = Path.Combine(tmpReleaseDir, $"{id}-Setup-[win-x64].exe"); Assert.True(File.Exists(setupPath1)); - var result = PlatformUtil.InvokeProcess(setupPath1, new string[] { "--nocolor", "--silent", "--installto", tmpInstallDir }, Environment.CurrentDirectory, CancellationToken.None); - logger.Info(result.StdOutput); - Assert.Equal(0, result.ExitCode); + RunProcess(setupPath1, new[] { "--nocolor", "--silent", "--installto", tmpInstallDir }, Environment.CurrentDirectory, logger); var updatePath = Path.Combine(tmpInstallDir, "Update.exe"); Assert.True(File.Exists(updatePath)); @@ -252,9 +251,7 @@ public class WindowsPackTests var date = DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture); Assert.Equal(date, installDate.Trim('\0')); - var result2 = PlatformUtil.InvokeProcess(updatePath, new string[] { "--nocolor", "--silent", "--uninstall" }, Environment.CurrentDirectory, CancellationToken.None); - logger.Info(result2.StdOutput); - Assert.Equal(0, result2.ExitCode); + RunProcess(updatePath, new string[] { "--nocolor", "--silent", "--uninstall" }, Environment.CurrentDirectory, logger); Assert.False(File.Exists(shortcutPath)); Assert.False(File.Exists(appPath)); @@ -264,4 +261,143 @@ public class WindowsPackTests Assert.Null(key2); } } + + [SkippableFact] + public void TestPackedAppCanDeltaUpdateToLatest() + { + Skip.IfNot(SquirrelRuntimeInfo.IsWindows); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var releaseDir); + using var _2 = Utility.GetTempDirectory(out var installDir); + + // pack v1 + PackTestApp("1.0.0", "version 1 test", releaseDir, logger); + + // install app + var setupPath1 = Path.Combine(releaseDir, $"{TEST_APP_ID}-Setup-[win-x64].exe"); + RunProcess(setupPath1, new string[] { "--nocolor", "--silent", "--installto", installDir }, Environment.GetFolderPath(Environment.SpecialFolder.Desktop), logger); + + // 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"); + Assert.True(File.Exists(argsPath)); + var argsContent = File.ReadAllText(argsPath).Trim(); + Assert.Equal("--squirrel-install 1.0.0", argsContent); + + // check app output + var chk1test = RunProcess(appPath, new string[] { "test" }, installDir, logger); + Assert.Equal("version 1 test", chk1test); + var chk1version = RunProcess(appPath, new string[] { "version" }, installDir, logger); + 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); + + // pack v2 + PackTestApp("2.0.0", "version 2 test", releaseDir, logger); + + // check can find v2 update + var chk2check = RunProcess(appPath, new string[] { "check", releaseDir }, installDir, logger); + Assert.EndsWith(Environment.NewLine + "update: 2.0.0", chk2check); + + // 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[] { "download", releaseDir }, installDir, logger); + RunProcess(appPath, new string[] { "apply", releaseDir }, installDir, logger); + + // check app output + var chk3test = RunProcess(appPath, new string[] { "test" }, installDir, logger); + Assert.Equal("version 3 test", chk3test); + var chk3version = RunProcess(appPath, new string[] { "version" }, installDir, logger); + 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); + + // 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); + + + + //var ch3download = RunProcess(appPath, new string[] { "check", releaseDir }, installDir, logger); + //logger.Info(ch3download.StdOutput); + //Assert.Equal(0, ch3download.ExitCode); + //Assert.Equal("update: 3.0.0", ch3download.StdOutput.Trim()); + //var ch3apply = RunProcess(appPath, new string[] { "check", releaseDir }, installDir, logger); + //logger.Info(ch3apply.StdOutput); + //Assert.Equal(0, ch3apply.ExitCode); + //Assert.Equal("update: 3.0.0", ch3apply.StdOutput.Trim()); + + + + // uninstall + var updatePath = Path.Combine(installDir, "Update.exe"); + RunProcess(updatePath, new string[] { "--nocolor", "--silent", "--uninstall" }, Environment.CurrentDirectory, logger); + } + + const string TEST_APP_ID = "Test.Squirrel-App"; + + private string RunProcess(string exe, string[] args, string workingDir, ILogger logger, int exitCode = 0) + { + var psi = new ProcessStartInfo(exe); + psi.WorkingDirectory = workingDir; + psi.CreateNoWindow = true; + psi.RedirectStandardOutput = true; + psi.RedirectStandardError = true; + psi.ArgumentList.AddRange(args); + var p = Process.Start(psi); + + StringBuilder sb = new StringBuilder(); + p.BeginErrorReadLine(); + p.BeginOutputReadLine(); + p.OutputDataReceived += (s, e) => { sb.AppendLine(e.Data); logger.Debug(e.Data); }; + p.ErrorDataReceived += (s, e) => { sb.AppendLine(e.Data); logger.Debug(e.Data); }; + p.WaitForExit(); + + Assert.Equal(exitCode, p.ExitCode); + return sb.ToString().Trim(); + } + + private void PackTestApp(string version, string testString, string releaseDir, ILogger logger) + { + var projDir = GetPath("TestApp"); + var testStringFile = Path.Combine(projDir, "Const.cs"); + + var oldText = File.ReadAllText(testStringFile); + File.WriteAllText(testStringFile, $"class Const {{ public const string TEST_STRING = \"{testString}\"; }}"); + var args = new string[] { "publish", "--no-self-contained", "-c", "Release", "-r", "win-x64", "-o", "publish" }; + RunProcess("dotnet", args, projDir, logger); + File.WriteAllText(testStringFile, oldText); + + var options = new WindowsPackOptions { + EntryExecutableName = "TestApp.exe", + ReleaseDir = new DirectoryInfo(releaseDir), + PackId = TEST_APP_ID, + PackVersion = version, + TargetRuntime = RID.Parse("win-x64"), + PackDirectory = Path.Combine(projDir, "publish"), + }; + + var runner = new WindowsPackCommandRunner(logger); + runner.Pack(options); + } + + private static string GetPath(params string[] paths) + { + var ret = GetIntegrationTestRootDirectory(); + return (new FileInfo(paths.Aggregate(ret, Path.Combine))).FullName; + } + + private static string GetIntegrationTestRootDirectory() + { + var st = new StackFrame(true); + var di = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(st.GetFileName()), "..")); + return di.FullName; + } } diff --git a/test/Squirrel.Tests/UpdateManagerTests.cs b/test/Squirrel.Tests/UpdateManagerTests.cs index 1be4b309..39ee920d 100644 --- a/test/Squirrel.Tests/UpdateManagerTests.cs +++ b/test/Squirrel.Tests/UpdateManagerTests.cs @@ -172,7 +172,7 @@ namespace Squirrel.Tests Assert.True(SemanticVersion.Parse(version) == info.TargetFullRelease.Version); Assert.Equal(0, info.DeltasToTarget.Count()); - um.DownloadAndPrepareUpdates(info); + um.DownloadUpdates(info); var target = Path.Combine(packagesDir, $"{id}-{version}-full.nupkg"); Assert.True(File.Exists(target)); @@ -203,7 +203,7 @@ namespace Squirrel.Tests Assert.Equal(3, info.DeltasToTarget.Count()); Assert.NotNull(info.BaseRelease); - await um.DownloadAndPrepareUpdatesAsync(info); + await um.DownloadUpdatesAsync(info); var target = Path.Combine(packagesDir, $"{id}-{toVersion}-full.nupkg"); Assert.True(File.Exists(target)); } diff --git a/test/TestApp/ConsoleLogger.cs b/test/TestApp/ConsoleLogger.cs new file mode 100644 index 00000000..1e24352c --- /dev/null +++ b/test/TestApp/ConsoleLogger.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +class ConsoleLogger : ILogger +{ + public IDisposable BeginScope(TState state) where TState : notnull + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Console.WriteLine(formatter(state, exception)); + } +} \ No newline at end of file diff --git a/test/TestApp/Const.cs b/test/TestApp/Const.cs new file mode 100644 index 00000000..fd47e33a --- /dev/null +++ b/test/TestApp/Const.cs @@ -0,0 +1 @@ +class Const { public const string TEST_STRING = "Hello, World!"; } \ No newline at end of file diff --git a/test/TestApp/Program.cs b/test/TestApp/Program.cs new file mode 100644 index 00000000..990372f8 --- /dev/null +++ b/test/TestApp/Program.cs @@ -0,0 +1,64 @@ +using Squirrel; +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); + return 0; + } + + if (args.Length == 1 && args[0] == "version") { + var locator = SquirrelLocator.GetDefault(new ConsoleLogger()); + Console.WriteLine(locator.CurrentlyInstalledVersion); + return 0; + } + + if (args.Length == 1 && args[0] == "test") { + Console.WriteLine(Const.TEST_STRING); + return 0; + } + + if (args.Length == 2) { + if (args[0] == "check") { + var um = new UpdateManager(args[1], null, new ConsoleLogger()); + var info = um.CheckForUpdates(); + if (info == null) { + Console.WriteLine("no updates"); + return 0; + } else { + Console.WriteLine("update: " + info.TargetFullRelease.Version); + return 0; + } + } + + if (args[0] == "download") { + var um = new UpdateManager(args[1], null, new ConsoleLogger()); + var info = um.CheckForUpdates(); + if (info == null) { + Console.WriteLine("no updates"); + return -1; + } + um.DownloadUpdates(info, (x) => Console.WriteLine(x)); + return 0; + } + + if (args[0] == "apply") { + var um = new UpdateManager(args[1], null, new ConsoleLogger()); + if (!um.IsUpdatePendingRestart) { + Console.WriteLine("not pending restart"); + return -1; + } + Console.WriteLine("applying..."); + um.ApplyUpdatesAndExit(); + return 0; + } + } + +} catch (Exception ex) { + Console.WriteLine("exception: " + ex.ToString()); + return -1; +} + +Console.WriteLine("Invalid args: " + String.Join(", ", args)); +return -1; \ No newline at end of file diff --git a/test/TestApp/TestApp.csproj b/test/TestApp/TestApp.csproj new file mode 100644 index 00000000..6150d475 --- /dev/null +++ b/test/TestApp/TestApp.csproj @@ -0,0 +1,13 @@ + + + + Exe + net6.0 + enable + + + + + + +