Implement AppDesc for win/macos

This commit is contained in:
Caelan Sayler
2022-05-14 10:33:44 +01:00
parent c015d1c9a2
commit 2d9c7cc37d
11 changed files with 552 additions and 500 deletions

View File

@@ -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:

404
src/Squirrel/AppDesc.cs Normal file
View File

@@ -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
{
/// <summary>
/// A base class describing where Squirrel can find key folders and files.
/// </summary>
public abstract class AppDesc : IEnableLogger
{
/// <summary>
/// Auto-detect the platform from the current operating system.
/// </summary>
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.");
}
/// <summary>
/// Instantiate base class <see cref="AppDesc"/>.
/// </summary>
protected AppDesc()
{
}
/// <summary> The unique application Id. This is used in various app paths. </summary>
public abstract string AppId { get; }
/// <summary>
/// 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.
/// </summary>
public abstract string RootAppDir { get; }
/// <summary> The directory in which nupkg files are stored for this application. </summary>
public abstract string PackagesDir { get; }
/// <summary> The temporary directory for this application. </summary>
public abstract string AppTempDir { get; }
/// <summary> True if the current binary is Update.exe within the specified application. </summary>
public abstract bool IsUpdateExe { get; }
/// <summary> The directory where new versions are stored, before they are applied. </summary>
public abstract string VersionStagingDir { get; }
/// <summary>
/// The directory where the current version of the application is stored.
/// This directory will be swapped out for a new version in <see cref="VersionStagingDir"/>.
/// </summary>
public abstract string CurrentVersionDir { get; }
/// <summary> The path to the current Update.exe or similar on other operating systems. </summary>
public abstract string UpdateExePath { get; }
/// <summary> The path to the RELEASES index detailing the local packages. </summary>
public virtual string ReleasesFilePath => Path.Combine(PackagesDir, "RELEASES");
/// <summary> The path to the .betaId file which contains a unique GUID for this user. </summary>
public virtual string BetaIdFilePath => Path.Combine(PackagesDir, ".betaId");
/// <summary> The currently installed version of the application. </summary>
public abstract SemanticVersion CurrentlyInstalledVersion { get; }
/// <summary>
/// Gets a
/// </summary>
/// <param name="version">The application version</param>
/// <returns>The full path to the version staging directory</returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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<string> 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<VersionDirInfo> 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();
}
}
/// <summary>
/// An implementation for Windows which uses the Squirrel defaults and installs to
/// local app data.
/// </summary>
public class AppDescWindows : AppDesc
{
/// <inheritdoc />
public override string AppId { get; }
/// <inheritdoc />
public override string RootAppDir { get; }
/// <inheritdoc />
public override string UpdateExePath { get; }
/// <inheritdoc />
public override bool IsUpdateExe { get; }
/// <inheritdoc />
public override SemanticVersion CurrentlyInstalledVersion { get; }
/// <inheritdoc />
public override string PackagesDir => CreateSubDirIfDoesNotExist(RootAppDir, "packages");
/// <inheritdoc />
public override string AppTempDir => CreateSubDirIfDoesNotExist(PackagesDir, "SquirrelClowdTemp");
/// <inheritdoc />
public override string VersionStagingDir => CreateSubDirIfDoesNotExist(RootAppDir, "staging");
/// <inheritdoc />
public override string CurrentVersionDir => CreateSubDirIfDoesNotExist(RootAppDir, "current");
/// <summary>
/// Creates a new Platform and tries to auto-detect the application details from
/// the current context.
/// </summary>
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;
}
}
}
/// <summary>
/// Creates a new windows application platform at the specified app directory.
/// </summary>
/// <param name="appDir">The location of the application.</param>
/// <param name="appId">The unique ID of the application.</param>
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;
}
}
}
/// <summary>
/// The default for OSX. All application files will remain in the '.app'.
/// All additional files (log, etc) will be placed in a temporary directory.
/// </summary>
public class AppDescOsx : AppDesc
{
/// <inheritdoc />
public override string AppId { get; }
/// <inheritdoc />
public override string RootAppDir { get; }
/// <inheritdoc />
public override string UpdateExePath { get; }
/// <inheritdoc />
public override bool IsUpdateExe { get; }
/// <inheritdoc />
public override string CurrentVersionDir => RootAppDir;
/// <inheritdoc />
public override SemanticVersion CurrentlyInstalledVersion { get; }
/// <inheritdoc />
public override string AppTempDir => CreateSubDirIfDoesNotExist(Utility.GetDefaultTempBaseDirectory(), AppId);
/// <inheritdoc />
public override string PackagesDir => CreateSubDirIfDoesNotExist(AppTempDir, "packages");
/// <inheritdoc />
public override string VersionStagingDir => CreateSubDirIfDoesNotExist(AppTempDir, "staging");
/// <summary>
/// Creates a new <see cref="AppDescOsx"/> and auto-detects the
/// app information from metadata embedded in the .app.
/// </summary>
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);
}
}
}
}

View File

@@ -1,77 +0,0 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Squirrel.Sources;
namespace Squirrel
{
/// <summary>
/// An implementation of UpdateManager which supports checking updates and
/// downloading releases directly from GitHub releases. This class is just a shorthand
/// for initialising <see cref="UpdateManager"/> with a <see cref="GithubSource"/>
/// as the first argument.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use 'new UpdateManager(new GithubSource(...))' instead")]
public class GithubUpdateManager : UpdateManager
{
/// <inheritdoc cref="UpdateManager(string, string, string, IFileDownloader)"/>
/// <param name="repoUrl">
/// The URL of the GitHub repository to download releases from
/// (e.g. https://github.com/myuser/myrepo)
/// </param>
/// <param name="applicationIdOverride">
/// 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.
/// </param>
/// <param name="urlDownloader">
/// A custom file downloader, for using non-standard package sources or adding
/// proxy configurations.
/// </param>
/// <param name="localAppDataDirectoryOverride">
/// Provide a custom location for the system LocalAppData, it will be used
/// instead of <see cref="Environment.SpecialFolder.LocalApplicationData"/>.
/// </param>
/// <param name="prerelease">
/// If true, the latest pre-release will be downloaded. If false, the latest
/// stable release will be downloaded.
/// </param>
/// <param name="accessToken">
/// 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.
/// </param>
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
{
/// <summary>
/// This function is obsolete and will be removed in a future version,
/// see the <see cref="GithubUpdateManager" /> class for a replacement.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use 'new UpdateManager(new GithubSource(...))' instead")]
public static Task<UpdateManager> 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));
}
}
}

View File

@@ -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)
{
@@ -90,8 +91,60 @@ namespace Squirrel.NuGet
.ToArray();
}
[SupportedOSPlatform("windows")]
public static Task ExtractZipReleaseForInstall(string zipFilePath, string outFolder, string rootPackageFolder, Action<int> 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<int> 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<int> 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);

View File

@@ -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
{
/// <summary> Describes how the application will be installed / updated on the given system. </summary>
public class UpdateConfig
{
/// <summary> The unique application Id. This is used in various app paths. </summary>
public virtual string AppId { get; }
/// <summary>
/// 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.
/// </summary>
public virtual string RootAppDir { get; }
/// <summary> The directory in which nupkg files are stored for this application. </summary>
public virtual string PackagesDir { get; }
/// <summary> The temporary directory for this application. </summary>
public virtual string TempDir { get; }
/// <summary> The directory where new versions are stored, before they are applied. </summary>
public virtual string VersionStagingDir { get; }
/// <summary>
/// The directory where the current version of the application is stored.
/// This directory will be swapped out for a new version in <see cref="VersionStagingDir"/>.
/// </summary>
public virtual string CurrentVersionDir { get; }
/// <summary> The path to the current Update.exe or similar on other operating systems. </summary>
public virtual string UpdateExePath { get; }
/// <summary> The path to the RELEASES index detailing the local packages. </summary>
public virtual string ReleasesFilePath => Path.Combine(PackagesDir, "RELEASES");
/// <summary> The path to the .betaId file which contains a unique GUID for this user. </summary>
public virtual string BetaIdFilePath => Path.Combine(PackagesDir, ".betaId");
/// <summary> The currently installed version of the application. </summary>
public virtual SemanticVersion CurrentlyInstalledVersion => GetCurrentlyInstalledVersion();
private static IFullLogger Log() => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(UpdateConfig));
/// <summary> Creates a new instance of UpdateConfig. </summary>
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<VersionDirInfo> 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));
}
}
}

View File

@@ -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) {

View File

@@ -25,88 +25,63 @@ namespace Squirrel
/// <inheritdoc/>
public virtual string AppDirectory => _config.RootAppDir;
/// <summary>The <see cref="UpdateConfig"/> describes the structure of the application on disk (eg. file/folder locations).</summary>
public UpdateConfig Config => _config;
/// <summary>The <see cref="AppDesc"/> describes the structure of the application on disk (eg. file/folder locations).</summary>
public AppDesc Config => _config;
/// <summary>The <see cref="IUpdateSource"/> responsible for retrieving updates from a package repository.</summary>
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;
/// <summary>
/// Create a new instance of <see cref="UpdateManager"/> to check for and install updates.
/// Do not forget to dispose this class! This constructor is just a shortcut for
/// <see cref="UpdateManager(IUpdateSource, string, string)"/>, and will automatically create
/// a <see cref="SimpleFileSource"/> or a <see cref="SimpleWebSource"/> depending on
/// whether 'urlOrPath' is a filepath or a URL, respectively.
/// Do not forget to dispose this class!
/// </summary>
/// <param name="urlOrPath">
/// 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)
/// </param>
/// <param name="applicationIdOverride">
/// 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.
/// </param>
/// <param name="localAppDataDirectoryOverride">
/// Provide a custom location for the system LocalAppData, it will be used
/// instead of <see cref="Environment.SpecialFolder.LocalApplicationData"/>.
/// </param>
/// <param name="urlDownloader">
/// A custom file downloader, for using non-standard package sources or adding proxy configurations.
/// </param>
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))
{
}
/// <summary>
/// Create a new instance of <see cref="UpdateManager"/> to check for and install updates.
/// Do not forget to dispose this class!
/// </summary>
/// <param name="updateSource">
/// <param name="source">
/// The source of your update packages. This can be a web server (<see cref="SimpleWebSource"/>),
/// a local directory (<see cref="SimpleFileSource"/>), a GitHub repository (<see cref="GithubSource"/>),
/// or a custom location.
/// </param>
/// <param name="applicationIdOverride">
/// 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.
/// </param>
/// <param name="localAppDataDirectoryOverride">
/// Provide a custom location for the system LocalAppData, it will be used
/// instead of <see cref="Environment.SpecialFolder.LocalApplicationData"/>.
/// </param>
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)
/// <summary>
/// Create a new instance of <see cref="UpdateManager"/> to check for and install updates.
/// Do not forget to dispose this class!
/// </summary>
/// <param name="source">
/// The source of your update packages. This can be a web server (<see cref="SimpleWebSource"/>),
/// a local directory (<see cref="SimpleFileSource"/>), a GitHub repository (<see cref="GithubSource"/>),
/// or a custom location.
/// </param>
/// <param name="config">
/// For configuring advanced / custom deployment scenarios. Should not be used unless
/// you know what you are doing.
/// </param>
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() { }
@@ -176,7 +151,6 @@ namespace Squirrel
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);
}
/// <inheritdoc/>
@@ -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.</remarks>
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. </param>
/// <param name="arguments">Arguments to start the exe with</param>
/// <returns>The Update.exe process that is waiting for this process to exit</returns>
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. </param>
/// <param name="arguments">Arguments to start the exe with</param>
/// <returns>The Update.exe process that is waiting for this process to exit</returns>
public async Task<Process> RestartAppWhenExitedAsync(string exeToStart = null, string arguments = null)
public static async Task<Process> 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<string> 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");
}

View File

@@ -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<ILogManager>().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);
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();
}
}
}

View File

@@ -6,16 +6,17 @@ 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)
{
@@ -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);

View File

@@ -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;

View File

@@ -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 },