mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Implement AppDesc for win/macos
This commit is contained in:
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
@@ -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
404
src/Squirrel/AppDesc.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user