diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06128d03..4bedd576 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,16 +53,9 @@ jobs: with: dotnet-version: 6.0.* - name: Build SquirrelMac - run: dotnet publish -v minimal -c Release -r osx.10.12-x64 --self-contained ./src/Squirrel.CommandLine.OSX/Squirrel.CommandLine.OSX.csproj -o ./publish -# - name: Build SquirrelMac -# run: | -# dotnet publish -v minimal -c Release -r osx.10.12-x64 --self-contained ./src/Squirrel.CommandLine.OSX/Squirrel.CommandLine.OSX.csproj -o ./publish -# mv ./publish/SquirrelMac ./publish/SquirrelMac-x64 -# dotnet publish -v minimal -c Release -r osx.11.0-arm64 --self-contained=true ./src/Squirrel.CommandLine.OSX/Squirrel.CommandLine.OSX.csproj -o ./publish -# mkdir ./bundle -# lipo -create -output ./bundle/SquirrelMac ./publish/SquirrelMac ./publish/SquirrelMac-x64 + run: dotnet publish -v minimal --self-contained -c Release -r osx.10.12-x64 ./src/Squirrel.CommandLine.OSX/Squirrel.CommandLine.OSX.csproj -o ./publish - name: Build UpdateMac - run: dotnet publish -v minimal -c Release -r osx.10.12-x64 --self-contained ./src/Update.OSX/Update.OSX.csproj -o ./publish + run: dotnet publish -v minimal --self-contained -c Release -r osx.10.12-x64 ./src/Update.OSX/Update.OSX.csproj -o ./publish - name: Upload MacOS Artifacts uses: actions/upload-artifact@v3 with: diff --git a/src/Squirrel/AppDesc.cs b/src/Squirrel/AppDesc.cs new file mode 100644 index 00000000..7722722b --- /dev/null +++ b/src/Squirrel/AppDesc.cs @@ -0,0 +1,404 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using NuGet.Versioning; +using Squirrel.NuGet; +using Squirrel.SimpleSplat; + +namespace Squirrel +{ + /// + /// A base class describing where Squirrel can find key folders and files. + /// + public abstract class AppDesc : IEnableLogger + { + /// + /// Auto-detect the platform from the current operating system. + /// + public static AppDesc GetCurrentPlatform() + { + if (SquirrelRuntimeInfo.IsWindows) + return new AppDescWindows(); + + if (SquirrelRuntimeInfo.IsOSX) + return new AppDescOsx(); + + throw new NotSupportedException($"OS platform '{SquirrelRuntimeInfo.SystemOsName}' is not supported."); + } + + /// + /// Instantiate base class . + /// + protected AppDesc() + { + } + + /// The unique application Id. This is used in various app paths. + public abstract string AppId { get; } + + /// + /// The root directory of the application. On Windows, this folder contains all + /// the application files, but that may not be the case on other operating systems. + /// + public abstract string RootAppDir { get; } + + /// The directory in which nupkg files are stored for this application. + public abstract string PackagesDir { get; } + + /// The temporary directory for this application. + public abstract string AppTempDir { get; } + + /// True if the current binary is Update.exe within the specified application. + public abstract bool IsUpdateExe { get; } + + /// The directory where new versions are stored, before they are applied. + public abstract string VersionStagingDir { get; } + + /// + /// The directory where the current version of the application is stored. + /// This directory will be swapped out for a new version in . + /// + public abstract string CurrentVersionDir { get; } + + /// The path to the current Update.exe or similar on other operating systems. + public abstract string UpdateExePath { get; } + + /// The path to the RELEASES index detailing the local packages. + public virtual string ReleasesFilePath => Path.Combine(PackagesDir, "RELEASES"); + + /// The path to the .betaId file which contains a unique GUID for this user. + public virtual string BetaIdFilePath => Path.Combine(PackagesDir, ".betaId"); + + /// The currently installed version of the application. + public abstract SemanticVersion CurrentlyInstalledVersion { get; } + + /// + /// Gets a + /// + /// The application version + /// The full path to the version staging directory + public virtual string GetVersionStagingPath(SemanticVersion version) + { + return Path.Combine(VersionStagingDir, "app-" + version); + } + + internal List<(string PackagePath, SemanticVersion PackageVersion, bool IsDelta)> GetLocalPackages() + { + var query = from x in Directory.EnumerateFiles(PackagesDir, "*.nupkg") + let re = ReleaseEntry.ParseEntryFileName(x) + where re.Version != null + select (x, re.Version, re.IsDelta); + return query.ToList(); + } + + internal string UpdateAndRetrieveCurrentFolder(bool force) + { + try { + var releases = GetVersions(); + var latestVer = releases.OrderByDescending(m => m.Version).First(); + var currentVer = releases.FirstOrDefault(f => f.IsCurrent); + + // if the latest ver is already current, or it does not support + // being in a current directory. + if (latestVer.IsCurrent) { + this.Log().Info($"Current directory already pointing to latest version."); + return latestVer.DirectoryPath; + } + + if (force) { + this.Log().Info($"Killing running processes in '{RootAppDir}'."); + Utility.KillProcessesInDirectory(RootAppDir); + } + + // 'current' does exist, and it's wrong, so lets get rid of it + if (currentVer != default) { + string legacyVersionDir = GetVersionStagingPath(currentVer.Version); + this.Log().Info($"Moving '{currentVer.DirectoryPath}' to '{legacyVersionDir}'."); + Utility.Retry(() => Directory.Move(currentVer.DirectoryPath, legacyVersionDir)); + } + + // this directory does not support being named 'current' + if (latestVer.Manifest == null) { + this.Log().Info($"Cannot promote {latestVer.Version} as current as it has no manifest"); + return latestVer.DirectoryPath; + } + + // 'current' doesn't exist right now, lets move the latest version + var latestDir = CurrentVersionDir; + this.Log().Info($"Moving '{latestVer.DirectoryPath}' to '{latestDir}'."); + Utility.Retry(() => Directory.Move(latestVer.DirectoryPath, latestDir)); + + this.Log().Info("Running app in: " + latestDir); + return latestDir; + } catch (Exception e) { + var releases = GetVersions(); + string fallback = releases.OrderByDescending(m => m.Version).First().DirectoryPath; + var currentVer = releases.FirstOrDefault(f => f.IsCurrent); + if (currentVer != default && Directory.Exists(currentVer.DirectoryPath)) { + fallback = currentVer.DirectoryPath; + } + + this.Log().WarnException("Unable to update 'current' directory", e); + this.Log().Info("Running app in: " + fallback); + return fallback; + } + } + + /// + /// Given a base dir and a directory name, will create a new sub directory of that name. + /// Will return null if baseDir is null, or if baseDir does not exist. + /// + protected static string CreateSubDirIfDoesNotExist(string baseDir, string newDir) + { + if (String.IsNullOrEmpty(baseDir) || string.IsNullOrEmpty(newDir)) return null; + var infoBase = new DirectoryInfo(baseDir); + if (!infoBase.Exists) return null; + var info = new DirectoryInfo(Path.Combine(baseDir, newDir)); + if (!info.Exists) info.Create(); + return info.FullName; + } + + /// + /// Starts Update.exe with the correct arguments to restart this process. + /// Update.exe will wait for this process to exit, and apply any pending version updates + /// before re-launching the latest version. + /// + public virtual Process StartRestartingProcess(string exeToStart = null, string arguments = null) + { + // NB: Here's how this method works: + // + // 1. We're going to pass the *name* of our EXE and the params to + // Update.exe + // 2. Update.exe is going to grab our PID (via getting its parent), + // then wait for us to exit. + // 3. Return control and new Process back to caller and allow them to Exit as desired. + // 4. After our process exits, Update.exe unblocks, then we launch the app again, possibly + // launching a different version than we started with (this is why + // we take the app's *name* rather than a full path) + + exeToStart = exeToStart ?? Path.GetFileName(SquirrelRuntimeInfo.EntryExePath); + + List args = new() { + "--forceLatest", + "--processStartAndWait", + exeToStart, + }; + + if (arguments != null) { + args.Add("-a"); + args.Add(arguments); + } + + return ProcessUtil.StartNonBlocking(UpdateExePath, args, Path.GetDirectoryName(UpdateExePath)); + } + + internal VersionDirInfo GetLatestVersion() + { + return GetLatestVersion(GetVersions()); + } + + internal VersionDirInfo GetLatestVersion(IEnumerable versions) + { + return versions.OrderByDescending(r => r.Version).FirstOrDefault(); + } + + internal VersionDirInfo GetVersionInfoFromDirectory(string d) + { + bool isCurrent = Utility.FullPathEquals(d, CurrentVersionDir); + var directoryName = Path.GetFileName(d); + bool isExecuting = Utility.IsFileInDirectory(SquirrelRuntimeInfo.EntryExePath, d); + var manifest = Utility.ReadManifestFromVersionDir(d); + + if (manifest != null) { + return new(manifest, manifest.Version, d, isCurrent, isExecuting); + } + + if (Utility.PathPartStartsWith(directoryName, "app-") && NuGetVersion.TryParse(directoryName.Substring(4), out var ver)) { + return new(null, ver, d, isCurrent, isExecuting); + } + + return null; + } + + internal record VersionDirInfo(IPackage Manifest, SemanticVersion Version, string DirectoryPath, bool IsCurrent, bool IsExecuting); + + internal VersionDirInfo[] GetVersions() + { + return Directory.EnumerateDirectories(RootAppDir, "app-*", SearchOption.TopDirectoryOnly) + .Concat(Directory.EnumerateDirectories(VersionStagingDir, "app-*", SearchOption.TopDirectoryOnly)) + .Concat(new[] { CurrentVersionDir }) + .Select(Utility.NormalizePath) + .Distinct(SquirrelRuntimeInfo.PathStringComparer) + .Select(GetVersionInfoFromDirectory) + .Where(d => d != null) + .ToArray(); + } + } + + /// + /// An implementation for Windows which uses the Squirrel defaults and installs to + /// local app data. + /// + public class AppDescWindows : AppDesc + { + /// + public override string AppId { get; } + + /// + public override string RootAppDir { get; } + + /// + public override string UpdateExePath { get; } + + /// + public override bool IsUpdateExe { get; } + + /// + public override SemanticVersion CurrentlyInstalledVersion { get; } + + /// + public override string PackagesDir => CreateSubDirIfDoesNotExist(RootAppDir, "packages"); + + /// + public override string AppTempDir => CreateSubDirIfDoesNotExist(PackagesDir, "SquirrelClowdTemp"); + + + /// + public override string VersionStagingDir => CreateSubDirIfDoesNotExist(RootAppDir, "staging"); + + /// + public override string CurrentVersionDir => CreateSubDirIfDoesNotExist(RootAppDir, "current"); + + /// + /// Creates a new Platform and tries to auto-detect the application details from + /// the current context. + /// + public AppDescWindows() + { + if (!SquirrelRuntimeInfo.IsWindows) + throw new NotSupportedException("Cannot instantiate AppDescWindows on a non-Windows system."); + + var ourPath = SquirrelRuntimeInfo.EntryExePath; + var myDir = Path.GetDirectoryName(ourPath); + + // Am I update.exe at the application root? + if (ourPath != null && + Path.GetFileName(ourPath).Equals("update.exe", StringComparison.InvariantCultureIgnoreCase) && + ourPath.IndexOf("app-", StringComparison.InvariantCultureIgnoreCase) == -1 && + ourPath.IndexOf("SquirrelClowdTemp", StringComparison.InvariantCultureIgnoreCase) == -1) { + UpdateExePath = ourPath; + RootAppDir = myDir; + var ver = GetLatestVersion(); + if (ver != null) { + AppId = ver.Manifest?.Id ?? Path.GetFileName(myDir); + CurrentlyInstalledVersion = ver.Version; + IsUpdateExe = true; + } + } + + // Am I running from within an app-* or current dir? + var info = GetVersionInfoFromDirectory(myDir); + if (info != null) { + var updateExe = Path.Combine(myDir, "..\\Update.exe"); + var updateExe2 = Path.Combine(myDir, "..\\..\\Update.exe"); + string updateLocation = null; + + if (File.Exists(updateExe)) { + updateLocation = Path.GetFullPath(updateExe); + } else if (File.Exists(updateExe2)) { + updateLocation = Path.GetFullPath(updateExe2); + } + + if (updateLocation != null) { + UpdateExePath = updateLocation; + RootAppDir = Path.GetDirectoryName(updateLocation); + AppId = info.Manifest?.Id ?? Path.GetFileName(Path.GetDirectoryName(updateLocation)); + CurrentlyInstalledVersion = info.Version; + IsUpdateExe = false; + } + } + } + + /// + /// Creates a new windows application platform at the specified app directory. + /// + /// The location of the application. + /// The unique ID of the application. + public AppDescWindows(string appDir, string appId) + { + AppId = appId; + RootAppDir = appDir; + var updateExe = Path.Combine(appDir, "Update.exe"); + var ver = GetLatestVersion(); + + if (File.Exists(updateExe) && ver != null) { + UpdateExePath = updateExe; + CurrentlyInstalledVersion = ver.Version; + } + } + } + + /// + /// The default for OSX. All application files will remain in the '.app'. + /// All additional files (log, etc) will be placed in a temporary directory. + /// + public class AppDescOsx : AppDesc + { + /// + public override string AppId { get; } + + /// + public override string RootAppDir { get; } + + /// + public override string UpdateExePath { get; } + + /// + public override bool IsUpdateExe { get; } + + /// + public override string CurrentVersionDir => RootAppDir; + + /// + public override SemanticVersion CurrentlyInstalledVersion { get; } + + /// + public override string AppTempDir => CreateSubDirIfDoesNotExist(Utility.GetDefaultTempBaseDirectory(), AppId); + + /// + public override string PackagesDir => CreateSubDirIfDoesNotExist(AppTempDir, "packages"); + + /// + public override string VersionStagingDir => CreateSubDirIfDoesNotExist(AppTempDir, "staging"); + + /// + /// Creates a new and auto-detects the + /// app information from metadata embedded in the .app. + /// + public AppDescOsx() + { + if (!SquirrelRuntimeInfo.IsOSX) + throw new NotSupportedException("Cannot instantiate AppDescOsx on a non-osx system."); + + // are we inside a .app? + var ourPath = SquirrelRuntimeInfo.EntryExePath; + var ix = ourPath.IndexOf(".app/", StringComparison.InvariantCultureIgnoreCase); + if (ix < 0) return; + + var appPath = ourPath.Substring(0, ix + 5); + var contentsDir = Path.Combine(appPath, "Contents"); + var updateExe = Path.Combine(contentsDir, "UpdateMac"); + var info = GetVersionInfoFromDirectory(contentsDir); + + if (File.Exists(updateExe) && info?.Manifest != null) { + AppId = info.Manifest.Id; + RootAppDir = appPath; + UpdateExePath = updateExe; + CurrentlyInstalledVersion = info.Version; + IsUpdateExe = Utility.FullPathEquals(updateExe, ourPath); + } + } + } +} \ No newline at end of file diff --git a/src/Squirrel/GithubUpdateManager.cs b/src/Squirrel/GithubUpdateManager.cs deleted file mode 100644 index 627f7419..00000000 --- a/src/Squirrel/GithubUpdateManager.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.ComponentModel; -using System.Threading.Tasks; -using Squirrel.Sources; - -namespace Squirrel -{ - /// - /// An implementation of UpdateManager which supports checking updates and - /// downloading releases directly from GitHub releases. This class is just a shorthand - /// for initialising with a - /// as the first argument. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use 'new UpdateManager(new GithubSource(...))' instead")] - public class GithubUpdateManager : UpdateManager - { - /// - /// - /// The URL of the GitHub repository to download releases from - /// (e.g. https://github.com/myuser/myrepo) - /// - /// - /// The Id of your application should correspond with the - /// appdata directory name, and the Id used with Squirrel releasify/pack. - /// If left null/empty, will attempt to determine the current application Id - /// from the installed app location. - /// - /// - /// A custom file downloader, for using non-standard package sources or adding - /// proxy configurations. - /// - /// - /// Provide a custom location for the system LocalAppData, it will be used - /// instead of . - /// - /// - /// If true, the latest pre-release will be downloaded. If false, the latest - /// stable release will be downloaded. - /// - /// - /// The GitHub access token to use with the request to download releases. - /// If left empty, the GitHub rate limit for unauthenticated requests allows - /// for up to 60 requests per hour, limited by IP address. - /// - public GithubUpdateManager( - string repoUrl, - bool prerelease = false, - string accessToken = null, - string applicationIdOverride = null, - string localAppDataDirectoryOverride = null, - IFileDownloader urlDownloader = null) - : base(new GithubSource(repoUrl, accessToken, prerelease, urlDownloader), applicationIdOverride, localAppDataDirectoryOverride) - { - } - } - - public partial class UpdateManager - { - /// - /// This function is obsolete and will be removed in a future version, - /// see the class for a replacement. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use 'new UpdateManager(new GithubSource(...))' instead")] - public static Task GitHubUpdateManager( - string repoUrl, - string applicationName = null, - string rootDirectory = null, - IFileDownloader urlDownloader = null, - bool prerelease = false, - string accessToken = null) - { - return Task.FromResult(new UpdateManager(new GithubSource(repoUrl, accessToken, prerelease, urlDownloader), applicationName, rootDirectory)); - } - } -} diff --git a/src/Squirrel/NuGet/ZipPackage.cs b/src/Squirrel/NuGet/ZipPackage.cs index c3b14317..89a93335 100644 --- a/src/Squirrel/NuGet/ZipPackage.cs +++ b/src/Squirrel/NuGet/ZipPackage.cs @@ -27,7 +27,8 @@ namespace Squirrel.NuGet public byte[] AppIconBytes { get; private set; } public ZipPackage(string filePath) : this(File.OpenRead(filePath)) - { } + { + } public ZipPackage(Stream zipStream, bool leaveOpen = false) { @@ -62,7 +63,7 @@ namespace Squirrel.NuGet private ZipArchiveEntry GetManifestEntry(ZipArchive zip) { var manifest = zip.Entries - .FirstOrDefault(f => f.Key.EndsWith(NugetUtil.ManifestExtension, StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(f => f.Key.EndsWith(NugetUtil.ManifestExtension, StringComparison.OrdinalIgnoreCase)); if (manifest == null) throw new InvalidDataException("Invalid nupkg. Does not contain required '.nuspec' manifest."); @@ -73,11 +74,11 @@ namespace Squirrel.NuGet private IEnumerable GetPackageFiles(ZipArchive zip) { return from entry in zip.Entries - where !entry.IsDirectory - let uri = new Uri(entry.Key, UriKind.Relative) - let path = NugetUtil.GetPath(uri) - where IsPackageFile(path) - select new ZipPackageFile(uri); + where !entry.IsDirectory + let uri = new Uri(entry.Key, UriKind.Relative) + let path = NugetUtil.GetPath(uri) + where IsPackageFile(path) + select new ZipPackageFile(uri); } private string[] GetFrameworks(IEnumerable files) @@ -90,8 +91,60 @@ namespace Squirrel.NuGet .ToArray(); } - [SupportedOSPlatform("windows")] public static Task ExtractZipReleaseForInstall(string zipFilePath, string outFolder, string rootPackageFolder, Action progress) + { + if (SquirrelRuntimeInfo.IsWindows) + return ExtractZipReleaseForInstallWindows(zipFilePath, outFolder, rootPackageFolder, progress); + + if (SquirrelRuntimeInfo.IsOSX) + return ExtractZipReleaseForInstallOSX(zipFilePath, outFolder, rootPackageFolder, progress); + + throw new NotSupportedException("Platform not supported."); + } + + private static readonly Regex libFolderPattern = new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled); + + [SupportedOSPlatform("macos")] + public static Task ExtractZipReleaseForInstallOSX(string zipFilePath, string outFolder, string rootPackageFolder, Action progress) + { + return Task.Run(() => { + using (var za = ZipArchive.Open(zipFilePath)) + using (var reader = za.ExtractAllEntries()) { + var totalItems = za.Entries.Count; + var currentItem = 0; + + while (reader.MoveToNextEntry()) { + // Report progress early since we might be need to continue for non-matches + currentItem++; + var percentage = (currentItem * 100d) / totalItems; + progress((int) percentage); + + var parts = reader.Entry.Key.Split('\\', '/').Select(x => Uri.UnescapeDataString(x)); + var decoded = String.Join(Path.DirectorySeparatorChar.ToString(), parts); + + if (!libFolderPattern.IsMatch(decoded)) continue; + decoded = libFolderPattern.Replace(decoded, "", 1); + + var fullTargetFile = Path.Combine(outFolder, decoded); + var fullTargetDir = Path.GetDirectoryName(fullTargetFile); + Directory.CreateDirectory(fullTargetDir); + + Utility.Retry(() => { + if (reader.Entry.IsDirectory) { + Directory.CreateDirectory(fullTargetFile); + } else { + reader.WriteEntryToFile(fullTargetFile); + } + }, 5); + } + } + + progress(100); + }); + } + + [SupportedOSPlatform("windows")] + public static Task ExtractZipReleaseForInstallWindows(string zipFilePath, string outFolder, string rootPackageFolder, Action progress) { var re = new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); @@ -116,8 +169,8 @@ namespace Squirrel.NuGet var parts = reader.Entry.Key.Split('\\', '/').Select(x => Uri.UnescapeDataString(x)); var decoded = String.Join(Path.DirectorySeparatorChar.ToString(), parts); - if (!re.IsMatch(decoded)) continue; - decoded = re.Replace(decoded, "", 1); + if (!libFolderPattern.IsMatch(decoded)) continue; + decoded = libFolderPattern.Replace(decoded, "", 1); var fullTargetFile = Path.Combine(outFolder, decoded); var fullTargetDir = Path.GetDirectoryName(fullTargetFile); @@ -159,4 +212,4 @@ namespace Squirrel.NuGet }); } } -} +} \ No newline at end of file diff --git a/src/Squirrel/UpdateConfig.cs b/src/Squirrel/UpdateConfig.cs deleted file mode 100644 index f6b17d1c..00000000 --- a/src/Squirrel/UpdateConfig.cs +++ /dev/null @@ -1,248 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NuGet.Versioning; -using Squirrel.NuGet; -using Squirrel.SimpleSplat; - -namespace Squirrel -{ - /// Describes how the application will be installed / updated on the given system. - public class UpdateConfig - { - /// The unique application Id. This is used in various app paths. - public virtual string AppId { get; } - - /// - /// The root directory of the application. On Windows, this folder contains all - /// the application files, but that may not be the case on other operating systems. - /// - public virtual string RootAppDir { get; } - - /// The directory in which nupkg files are stored for this application. - public virtual string PackagesDir { get; } - - /// The temporary directory for this application. - public virtual string TempDir { get; } - - /// The directory where new versions are stored, before they are applied. - public virtual string VersionStagingDir { get; } - - /// - /// The directory where the current version of the application is stored. - /// This directory will be swapped out for a new version in . - /// - public virtual string CurrentVersionDir { get; } - - /// The path to the current Update.exe or similar on other operating systems. - public virtual string UpdateExePath { get; } - - /// The path to the RELEASES index detailing the local packages. - public virtual string ReleasesFilePath => Path.Combine(PackagesDir, "RELEASES"); - - /// The path to the .betaId file which contains a unique GUID for this user. - public virtual string BetaIdFilePath => Path.Combine(PackagesDir, ".betaId"); - - /// The currently installed version of the application. - public virtual SemanticVersion CurrentlyInstalledVersion => GetCurrentlyInstalledVersion(); - - private static IFullLogger Log() => SquirrelLocator.Current.GetService().GetLogger(typeof(UpdateConfig)); - - /// Creates a new instance of UpdateConfig. - public UpdateConfig(string applicationIdOverride, string localAppDataDirOverride) - { - UpdateExePath = GetUpdateExe(); - AppId = applicationIdOverride ?? GetInstalledApplicationName(UpdateExePath); - if (AppId != null) { - RootAppDir = Path.Combine(localAppDataDirOverride ?? GetLocalAppDataDirectory(), AppId); - CurrentVersionDir = Path.Combine(RootAppDir, "current"); - PackagesDir = Path.Combine(RootAppDir, "packages"); - VersionStagingDir = Path.Combine(RootAppDir, "packages"); - TempDir = Path.Combine(PackagesDir, "Temp"); - } - } - - internal List<(string PackagePath, SemanticVersion PackageVersion, bool IsDelta)> GetLocalPackages() - { - var query = from x in Directory.EnumerateFiles(PackagesDir, "*.nupkg") - let re = ReleaseEntry.ParseEntryFileName(x) - where re.Version != null - select (x, re.Version, re.IsDelta); - return query.ToList(); - } - - internal string GetVersionStagingPath(SemanticVersion version) - { - return Path.Combine(VersionStagingDir, "app-" + version); - } - - internal string UpdateAndRetrieveCurrentFolder(bool force) - { - try { - var releases = GetVersions(); - var latestVer = releases.OrderByDescending(m => m.Version).First(); - var currentVer = releases.FirstOrDefault(f => f.IsCurrent); - - // if the latest ver is already current, or it does not support - // being in a current directory. - if (latestVer.IsCurrent) { - Log().Info($"Current directory already pointing to latest version."); - return latestVer.DirectoryPath; - } - - if (force) { - Log().Info($"Killing running processes in '{RootAppDir}'."); - Utility.KillProcessesInDirectory(RootAppDir); - } - - // 'current' does exist, and it's wrong, so lets get rid of it - if (currentVer != default) { - string legacyVersionDir = GetVersionStagingPath(currentVer.Version); - Log().Info($"Moving '{currentVer.DirectoryPath}' to '{legacyVersionDir}'."); - Utility.Retry(() => Directory.Move(currentVer.DirectoryPath, legacyVersionDir)); - } - - // this directory does not support being named 'current' - if (latestVer.Manifest == null) { - Log().Info($"Cannot promote {latestVer.Version} as current as it has no manifest"); - return latestVer.DirectoryPath; - } - - // 'current' doesn't exist right now, lets move the latest version - var latestDir = CurrentVersionDir; - Log().Info($"Moving '{latestVer.DirectoryPath}' to '{latestDir}'."); - Utility.Retry(() => Directory.Move(latestVer.DirectoryPath, latestDir)); - - Log().Info("Running app in: " + latestDir); - return latestDir; - } catch (Exception e) { - var releases = GetVersions(); - string fallback = releases.OrderByDescending(m => m.Version).First().DirectoryPath; - var currentVer = releases.FirstOrDefault(f => f.IsCurrent); - if (currentVer != default && Directory.Exists(currentVer.DirectoryPath)) { - fallback = currentVer.DirectoryPath; - } - Log().WarnException("Unable to update 'current' directory", e); - Log().Info("Running app in: " + fallback); - return fallback; - } - } - - internal VersionDirInfo GetLatestVersion() - { - return GetLatestVersion(GetVersions()); - } - - internal VersionDirInfo GetLatestVersion(IEnumerable versions) - { - return versions.OrderByDescending(r => r.Version).First(); - } - - internal VersionDirInfo GetVersionInfoFromDirectory(string d) - { - bool isCurrent = Utility.FullPathEquals(d, CurrentVersionDir); - var directoryName = Path.GetFileName(d); - bool isExecuting = Utility.IsFileInDirectory(SquirrelRuntimeInfo.EntryExePath, d); - var manifest = Utility.ReadManifestFromVersionDir(d); - if (manifest != null) { - return new(manifest, manifest.Version, d, isCurrent, isExecuting); - } else if (Utility.PathPartStartsWith(directoryName, "app-") && NuGetVersion.TryParse(directoryName.Substring(4), out var ver)) { - return new(null, ver, d, isCurrent, isExecuting); - } - return null; - } - - internal record VersionDirInfo(IPackage Manifest, SemanticVersion Version, string DirectoryPath, bool IsCurrent, bool IsExecuting); - internal VersionDirInfo[] GetVersions() - { - return Directory.EnumerateDirectories(RootAppDir, "app-*", SearchOption.TopDirectoryOnly) - .Concat(Directory.EnumerateDirectories(VersionStagingDir, "app-*", SearchOption.TopDirectoryOnly)) - .Concat(new[] { CurrentVersionDir }) - .Select(Utility.NormalizePath) - .Distinct(SquirrelRuntimeInfo.PathStringComparer) - .Select(GetVersionInfoFromDirectory) - .Where(d => d != null) - .ToArray(); - } - - private static string GetInstalledApplicationName(string updateExePath) - { - if (updateExePath == null) - return null; - var fi = new FileInfo(updateExePath); - return fi.Directory.Name; - } - - private static string GetUpdateExe() - { - var ourPath = SquirrelRuntimeInfo.EntryExePath; - - // Are we update.exe? - if (ourPath != null && - Path.GetFileName(ourPath).Equals("update.exe", StringComparison.OrdinalIgnoreCase) && - ourPath.IndexOf("app-", StringComparison.OrdinalIgnoreCase) == -1 && - ourPath.IndexOf("SquirrelTemp", StringComparison.OrdinalIgnoreCase) == -1) { - return Path.GetFullPath(ourPath); - } - - var updateDotExe = Path.Combine(SquirrelRuntimeInfo.BaseDirectory, "..\\Update.exe"); - var target = new FileInfo(updateDotExe); - - if (!target.Exists) - return null; - return target.FullName; - } - - private static string GetLocalAppDataDirectory() - { - // if we're installed and running as update.exe in the app folder, the app directory root is one folder up - if (SquirrelRuntimeInfo.IsSingleFile && Path.GetFileName(SquirrelRuntimeInfo.EntryExePath).Equals("Update.exe", StringComparison.OrdinalIgnoreCase)) { - var oneFolderUpFromAppFolder = Path.Combine(Path.GetDirectoryName(SquirrelRuntimeInfo.EntryExePath), ".."); - return Path.GetFullPath(oneFolderUpFromAppFolder); - } - - // if update exists above us, we're running from within a version directory, and the appdata folder is two above us - if (File.Exists(Path.Combine(SquirrelRuntimeInfo.BaseDirectory, "..", "Update.exe"))) { - var twoFoldersUpFromAppFolder = Path.Combine(Path.GetDirectoryName(SquirrelRuntimeInfo.EntryExePath), "..\\.."); - return Path.GetFullPath(twoFoldersUpFromAppFolder); - } - - // if neither of the above are true, we're probably not installed yet, so return the real appdata directory - return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - } - - private SemanticVersion GetCurrentlyInstalledVersion(string executable = null) - { - if (String.IsNullOrEmpty(RootAppDir) || String.IsNullOrEmpty(UpdateExePath)) - return null; - - executable = Path.GetFullPath(executable ?? SquirrelRuntimeInfo.EntryExePath); - - // check if the application to check is in the correct application directory - if (!Utility.IsFileInDirectory(executable, RootAppDir)) - return null; - - // check if Update.exe exists in the expected relative location - var baseDir = Path.GetDirectoryName(executable); - if (!File.Exists(Path.Combine(baseDir, "..\\Update.exe"))) - return null; - - // if a 'my version' file exists, use that instead. - var manifest = Utility.ReadManifestFromVersionDir(baseDir); - if (manifest != null) { - return manifest.Version; - } - - var exePathWithoutAppDir = executable.Substring(RootAppDir.Length); - var appDirName = exePathWithoutAppDir.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) - .FirstOrDefault(x => x.StartsWith("app-", StringComparison.OrdinalIgnoreCase)); - - // check if we are inside an 'app-{ver}' directory and extract version - if (appDirName == null) - return null; - - return NuGetVersion.Parse(appDirName.Substring(4)); - } - } -} diff --git a/src/Squirrel/UpdateManager.ApplyReleases.cs b/src/Squirrel/UpdateManager.ApplyReleases.cs index 5bd9ccbe..2a755398 100644 --- a/src/Squirrel/UpdateManager.ApplyReleases.cs +++ b/src/Squirrel/UpdateManager.ApplyReleases.cs @@ -375,8 +375,8 @@ namespace Squirrel Contract.Requires(deltaPackageZip != null); Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile)); - using (Utility.GetTempDirectory(out var deltaPath, _config.TempDir)) - using (Utility.GetTempDirectory(out var workingPath, _config.TempDir)) { + using (Utility.GetTempDirectory(out var deltaPath, _config.AppTempDir)) + using (Utility.GetTempDirectory(out var workingPath, _config.AppTempDir)) { EasyZip.ExtractZipToDirectory(deltaPackageZip, deltaPath); progress(25); @@ -438,7 +438,7 @@ namespace Squirrel var inputFile = Path.Combine(deltaPath, relativeFilePath); var finalTarget = Path.Combine(workingDirectory, Regex.Replace(relativeFilePath, @"\.(bs)?diff$", "")); - using var _d = Utility.GetTempFileName(out var tempTargetFile, _config.TempDir); + using var _d = Utility.GetTempFileName(out var tempTargetFile, _config.AppTempDir); // NB: Zero-length diffs indicate the file hasn't actually changed if (new FileInfo(inputFile).Length == 0) { diff --git a/src/Squirrel/UpdateManager.cs b/src/Squirrel/UpdateManager.cs index edaabc76..e1acd657 100644 --- a/src/Squirrel/UpdateManager.cs +++ b/src/Squirrel/UpdateManager.cs @@ -25,88 +25,63 @@ namespace Squirrel /// public virtual string AppDirectory => _config.RootAppDir; - /// The describes the structure of the application on disk (eg. file/folder locations). - public UpdateConfig Config => _config; + /// The describes the structure of the application on disk (eg. file/folder locations). + public AppDesc Config => _config; /// The responsible for retrieving updates from a package repository. public IUpdateSource Source => _source; private readonly IUpdateSource _source; - private readonly UpdateConfig _config; + private readonly AppDesc _config; private readonly object _lockobj = new object(); private IDisposable _updateLock; private bool _disposed; /// /// Create a new instance of to check for and install updates. - /// Do not forget to dispose this class! This constructor is just a shortcut for - /// , and will automatically create - /// a or a depending on - /// whether 'urlOrPath' is a filepath or a URL, respectively. + /// Do not forget to dispose this class! /// /// - /// The URL where your update packages or stored, or a local package repository directory. + /// The URL or local directory that contains application update files (.nupkg and RELEASES) /// - /// - /// The Id of your application should correspond with the - /// appdata directory name, and the Id used with Squirrel releasify/pack. - /// If left null/empty, UpdateManger will attempt to determine the current application Id - /// from the installed app location, or throw if the app is not currently installed during certain - /// operations. - /// - /// - /// Provide a custom location for the system LocalAppData, it will be used - /// instead of . - /// - /// - /// A custom file downloader, for using non-standard package sources or adding proxy configurations. - /// - public UpdateManager( - string urlOrPath, - string applicationIdOverride = null, - string localAppDataDirectoryOverride = null, - IFileDownloader urlDownloader = null) - : this(CreateSource(urlOrPath, urlDownloader), applicationIdOverride, localAppDataDirectoryOverride) - { } + public UpdateManager(string urlOrPath) : this(CreateSource(urlOrPath)) + { + } /// /// Create a new instance of to check for and install updates. /// Do not forget to dispose this class! /// - /// + /// /// The source of your update packages. This can be a web server (), /// a local directory (), a GitHub repository (), /// or a custom location. /// - /// - /// The Id of your application should correspond with the - /// appdata directory name, and the Id used with Squirrel releasify/pack. - /// If left null/empty, UpdateManger will attempt to determine the current application Id - /// from the installed app location, or throw if the app is not currently installed during certain - /// operations. - /// - /// - /// Provide a custom location for the system LocalAppData, it will be used - /// instead of . - /// - public UpdateManager( - IUpdateSource updateSource, - string applicationIdOverride = null, - string localAppDataDirectoryOverride = null) - : this(updateSource, new UpdateConfig(applicationIdOverride, localAppDataDirectoryOverride)) - { } + public UpdateManager(IUpdateSource source) : this(source, null) + { + } - public UpdateManager( - string urlOrPath, - UpdateConfig config, - IFileDownloader urlDownloader = null) - : this(CreateSource(urlOrPath, urlDownloader), config) - { } - - public UpdateManager(IUpdateSource source, UpdateConfig config) + /// + /// Create a new instance of to check for and install updates. + /// Do not forget to dispose this class! + /// + /// + /// The source of your update packages. This can be a web server (), + /// a local directory (), a GitHub repository (), + /// or a custom location. + /// + /// + /// For configuring advanced / custom deployment scenarios. Should not be used unless + /// you know what you are doing. + /// + public UpdateManager(IUpdateSource source, AppDesc config) { _source = source; - _config = config; + _config = config ?? AppDesc.GetCurrentPlatform(); + } + + internal UpdateManager(string urlOrPath, string appId) : this(CreateSource(urlOrPath), new AppDescWindows()) + { } internal UpdateManager() { } @@ -140,7 +115,7 @@ namespace Squirrel bool ignoreDeltaUpdates = false; - retry: + retry: var updateInfo = default(UpdateInfo); try { @@ -166,17 +141,16 @@ namespace Squirrel } await this.ErrorIfThrows(() => - DownloadReleases(updateInfo.ReleasesToApply, x => progress(x / 3 + 33)), + DownloadReleases(updateInfo.ReleasesToApply, x => progress(x / 3 + 33)), "Failed to download updates").ConfigureAwait(false); await this.ErrorIfThrows(() => - ApplyReleases(updateInfo, x => progress(x / 3 + 66)), + ApplyReleases(updateInfo, x => progress(x / 3 + 66)), "Failed to apply updates").ConfigureAwait(false); if (SquirrelRuntimeInfo.IsWindows) { await CreateUninstallerRegistryEntry().ConfigureAwait(false); } - } catch { if (ignoreDeltaUpdates == false) { ignoreDeltaUpdates = true; @@ -186,9 +160,7 @@ namespace Squirrel throw; } - return updateInfo.ReleasesToApply.Any() ? - updateInfo.ReleasesToApply.MaxBy(x => x.Version).Last() : - default(ReleaseEntry); + return updateInfo.ReleasesToApply.Any() ? updateInfo.ReleasesToApply.MaxBy(x => x.Version).Last() : default(ReleaseEntry); } /// @@ -205,6 +177,7 @@ namespace Squirrel if (disp != null) { disp.Dispose(); } + _disposed = true; GC.SuppressFinalize(this); } @@ -222,7 +195,7 @@ namespace Squirrel /// however you'd like. public void RestartApp(string exeToStart = null, string arguments = null) { - restartProcess(exeToStart, arguments); + AppDesc.GetCurrentPlatform().StartRestartingProcess(exeToStart, arguments); // NB: We have to give update.exe some time to grab our PID Thread.Sleep(500); Environment.Exit(0); @@ -236,9 +209,9 @@ namespace Squirrel /// the current executable. /// Arguments to start the exe with /// The Update.exe process that is waiting for this process to exit - public Process RestartAppWhenExited(string exeToStart = null, string arguments = null) + public static Process RestartAppWhenExited(string exeToStart = null, string arguments = null) { - var process = restartProcess(exeToStart, arguments); + var process = AppDesc.GetCurrentPlatform().StartRestartingProcess(exeToStart, arguments); // NB: We have to give update.exe some time to grab our PID Thread.Sleep(500); return process; @@ -252,63 +225,16 @@ namespace Squirrel /// the current executable. /// Arguments to start the exe with /// The Update.exe process that is waiting for this process to exit - public async Task RestartAppWhenExitedAsync(string exeToStart = null, string arguments = null) + public static async Task RestartAppWhenExitedAsync(string exeToStart = null, string arguments = null) { - var process = restartProcess(exeToStart, arguments); + var process = AppDesc.GetCurrentPlatform().StartRestartingProcess(exeToStart, arguments); // NB: We have to give update.exe some time to grab our PID await Task.Delay(500).ConfigureAwait(false); return process; } - [SupportedOSPlatform("windows")] - private Process restartProcess(string exeToStart = null, string arguments = null) - { - // NB: Here's how this method works: - // - // 1. We're going to pass the *name* of our EXE and the params to - // Update.exe - // 2. Update.exe is going to grab our PID (via getting its parent), - // then wait for us to exit. - // 3. Return control and new Process back to caller and allow them to Exit as desired. - // 4. After our process exits, Update.exe unblocks, then we launch the app again, possibly - // launching a different version than we started with (this is why - // we take the app's *name* rather than a full path) - exeToStart = exeToStart ?? Path.GetFileName(SquirrelRuntimeInfo.EntryExePath); - - List args = new() { - "--forceLatest", - "--processStartAndWait", - exeToStart, - }; - - if (arguments != null) { - args.Add("-a"); - args.Add(arguments); - } - - return ProcessUtil.StartNonBlocking(_config.UpdateExePath, args, Path.GetDirectoryName(_config.UpdateExePath)); - } - - private static string GetLocalAppDataDirectory(string assemblyLocation = null) - { - // if we're installed and running as update.exe in the app folder, the app directory root is one folder up - if (SquirrelRuntimeInfo.IsSingleFile && Path.GetFileName(SquirrelRuntimeInfo.EntryExePath).Equals("Update.exe", StringComparison.OrdinalIgnoreCase)) { - var oneFolderUpFromAppFolder = Path.Combine(Path.GetDirectoryName(SquirrelRuntimeInfo.EntryExePath), ".."); - return Path.GetFullPath(oneFolderUpFromAppFolder); - } - - // if update exists above us, we're running from within a version directory, and the appdata folder is two above us - if (File.Exists(Path.Combine(SquirrelRuntimeInfo.BaseDirectory, "..", "Update.exe"))) { - var twoFoldersUpFromAppFolder = Path.Combine(Path.GetDirectoryName(SquirrelRuntimeInfo.EntryExePath), "..\\.."); - return Path.GetFullPath(twoFoldersUpFromAppFolder); - } - - // if neither of the above are true, we're probably not installed yet, so return the real appdata directory - return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - } - - private static IUpdateSource CreateSource(string urlOrPath, IFileDownloader urlDownloader) + private static IUpdateSource CreateSource(string urlOrPath, IFileDownloader urlDownloader = null) { if (String.IsNullOrWhiteSpace(urlOrPath)) { return null; @@ -333,8 +259,7 @@ namespace Squirrel IDisposable theLock; try { - theLock = ModeDetector.InUnitTestRunner() ? - Disposable.Create(() => { }) : new SingleGlobalInstance(key, TimeSpan.FromMilliseconds(2000)); + theLock = ModeDetector.InUnitTestRunner() ? Disposable.Create(() => { }) : new SingleGlobalInstance(key, TimeSpan.FromMilliseconds(2000)); } catch (TimeoutException) { throw new TimeoutException("Couldn't acquire update lock, another instance may be running updates"); } @@ -370,4 +295,4 @@ namespace Squirrel return (int) totalPercentage; } } -} +} \ No newline at end of file diff --git a/src/Update.OSX/Program.cs b/src/Update.OSX/Program.cs index a5ed6501..486ed917 100644 --- a/src/Update.OSX/Program.cs +++ b/src/Update.OSX/Program.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Runtime.InteropServices; using System.Threading; using Squirrel.SimpleSplat; @@ -7,7 +8,6 @@ namespace Squirrel.Update { class Program : IEnableLogger { - static StartupOption _options; static IFullLogger Log => SquirrelLocator.Current.GetService().GetLogger(typeof(Program)); public static int Main(string[] args) @@ -16,19 +16,19 @@ namespace Squirrel.Update try { logger = new SetupLogLogger(Utility.GetDefaultTempBaseDirectory()); SquirrelLocator.CurrentMutable.Register(() => logger, typeof(ILogger)); - _options = new StartupOption(args); + var opt = new StartupOption(args); - if (_options.updateAction == UpdateAction.Unset) { - _options.WriteOptionDescriptions(); + if (opt.updateAction == UpdateAction.Unset) { + opt.WriteOptionDescriptions(); return -1; } Log.Info("Starting Squirrel Updater: " + String.Join(" ", args)); Log.Info("Updater location is: " + SquirrelRuntimeInfo.EntryExePath); - switch (_options.updateAction) { - case UpdateAction.ApplyLatest: - ApplyLatestVersion(_options.updateCurrentApp, _options.updateStagingDir, _options.restartApp); + switch (opt.updateAction) { + case UpdateAction.ProcessStart: + ProcessStart(opt.processStart, opt.processStartArgs, opt.shouldWait, opt.forceLatest); break; } @@ -41,21 +41,27 @@ namespace Squirrel.Update } } - static void ApplyLatestVersion(string currentDir, string stagingDir, bool restartApp) + static void ProcessStart(string exeName, string arguments, bool shouldWait, bool forceLatest) { - if (!Utility.FileHasExtension(currentDir, ".app")) { - throw new ArgumentException("The current dir must end with '.app' on macos."); - } + if (shouldWait) waitForParentToExit(); + // todo https://stackoverflow.com/questions/51441576/how-to-run-app-as-sudo // https://stackoverflow.com/questions/10283062/getting-sudo-to-ask-for-password-via-the-gui - Process.Start("killall", $"`basename -a '{currentDir}'`")?.WaitForExit(); + var desc = new AppDescOsx(); + var currentDir = desc.UpdateAndRetrieveCurrentFolder(forceLatest); - var config = new UpdateConfig(null, null); - config.UpdateAndRetrieveCurrentFolder(false); + ProcessUtil.InvokeProcess("open", new[] { "-n", currentDir }, null, CancellationToken.None); + } - if (restartApp) - ProcessUtil.InvokeProcess("open", new[] { "-n", currentDir }, null, CancellationToken.None); + [DllImport("libSystem.dylib")] + private static extern int getppid(); + + static void waitForParentToExit() + { + var parentPid = getppid(); + var proc = Process.GetProcessById(parentPid); + proc.WaitForExit(); } } } \ No newline at end of file diff --git a/src/Update.OSX/StartupOption.cs b/src/Update.OSX/StartupOption.cs index c72a7380..ffd4b283 100644 --- a/src/Update.OSX/StartupOption.cs +++ b/src/Update.OSX/StartupOption.cs @@ -6,17 +6,18 @@ namespace Squirrel.Update { enum UpdateAction { - Unset = 0, ApplyLatest + Unset = 0, ProcessStart } internal class StartupOption { private readonly OptionSet optionSet; internal UpdateAction updateAction { get; private set; } = default(UpdateAction); - internal string updateCurrentApp { get; private set; } - internal string updateStagingDir { get; private set; } - internal bool restartApp { get; private set; } - + internal string processStart { get; private set; } = default(string); + internal string processStartArgs { get; private set; } = default(string); + internal bool shouldWait { get; private set; } = false; + internal bool forceLatest { get; private set; } = false; + public StartupOption(string[] args) { optionSet = Parse(args); @@ -32,18 +33,14 @@ namespace Squirrel.Update #pragma warning restore CS0436 // Type conflicts with imported type $"Usage: {exeName} command [OPTS]", "", - "Commands:", { - "apply=", "Replace {0:CURRENT} .app with the latest in {1:STAGING}", - (v1, v2) => { - updateAction = UpdateAction.ApplyLatest; - updateCurrentApp = v1; - updateStagingDir = v2; - } - }, - { "restartApp", "Launch the app after applying the latest version", v => restartApp = true }, + "Commands:", + { "processStart=", "Start an executable in the current version of the app package", v => { updateAction = UpdateAction.ProcessStart; processStart = v; }, true}, + { "processStartAndWait=", "Start an executable in the current version of the app package", v => { updateAction = UpdateAction.ProcessStart; processStart = v; shouldWait = true; }, true}, "", "Options:", { "h|?|help", "Display Help and exit", _ => { } }, + { "forceLatest", "Force updates the current version folder", v => forceLatest = true}, + { "a=|process-start-args=", "Arguments that will be used when starting executable", v => processStartArgs = v, true}, }; opts.Parse(args); diff --git a/src/Update.Windows/Program.cs b/src/Update.Windows/Program.cs index 754a9a43..f2dd7940 100644 --- a/src/Update.Windows/Program.cs +++ b/src/Update.Windows/Program.cs @@ -445,7 +445,7 @@ namespace Squirrel.Update if (shouldWait) waitForParentToExit(); - var config = new UpdateConfig(null, null); + var config = new AppDescWindows(); var latestAppDir = config.UpdateAndRetrieveCurrentFolder(forceLatest); // Check for the EXE name they want @@ -586,7 +586,6 @@ namespace Squirrel.Update return true; } - static string getAppNameFromDirectory(string path = null) { path = path ?? SquirrelRuntimeInfo.BaseDirectory; diff --git a/src/Update.Windows/StartupOption.cs b/src/Update.Windows/StartupOption.cs index b127b373..7d289207 100644 --- a/src/Update.Windows/StartupOption.cs +++ b/src/Update.Windows/StartupOption.cs @@ -61,7 +61,7 @@ namespace Squirrel.Update { "s|silent", "Silent install", _ => silentInstall = true, true}, { "processStart=", "Start an executable in the current version of the app package", v => { updateAction = UpdateAction.ProcessStart; processStart = v; }, true}, { "processStartAndWait=", "Start an executable in the current version of the app package", v => { updateAction = UpdateAction.ProcessStart; processStart = v; shouldWait = true; }, true}, - { "forceLatest", "Force updates the current version junction", v => forceLatest = true}, + { "forceLatest", "Force updates the current version folder", v => forceLatest = true}, { "a=|process-start-args=", "Arguments that will be used when starting executable", v => processStartArgs = v, true}, { "setup=", "Install the package at this location", v => { updateAction = UpdateAction.Setup; target = v; }, true }, { "setupOffset=", "Offset where in setup package to start reading", v => { setupOffset = long.Parse(v); }, true },