mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
WIP: basic restructuring and renames
This commit is contained in:
22
Squirrel.sln
22
Squirrel.sln
@@ -18,14 +18,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionLevel", "SolutionLe
|
||||
version.json = version.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine", "src\Squirrel.CommandLine\Squirrel.CommandLine.csproj", "{352C15EA-622F-4132-80D8-9B6E3C83404E}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Packaging", "src\Squirrel.Packaging\Squirrel.Packaging.csproj", "{352C15EA-622F-4132-80D8-9B6E3C83404E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Tool", "src\Squirrel.Tool\Squirrel.Tool.csproj", "{9E769C7E-A54C-4844-8362-727D37BB1578}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.Csq", "src\Squirrel.Csq\Squirrel.Csq.csproj", "{9E769C7E-A54C-4844-8362-727D37BB1578}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine.Tests", "test\Squirrel.CommandLine.Tests\Squirrel.CommandLine.Tests.csproj", "{519EAB50-47B8-425F-8B20-AB9548F220B4}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{7AC3A776-B582-4B65-9D03-BD52332B5CA3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Packaging.Windows", "src\Squirrel.Packaging.Windows\Squirrel.Packaging.Windows.csproj", "{E35039C8-1F98-48EB-B7D5-08E33DF061A7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Packaging.OSX", "src\Squirrel.Packaging.OSX\Squirrel.Packaging.OSX.csproj", "{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Deployment", "src\Squirrel.Deployment\Squirrel.Deployment.csproj", "{D19EA72C-E7AE-4A7B-924A-E7550901A49C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -52,6 +58,18 @@ Global
|
||||
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{519EAB50-47B8-425F-8B20-AB9548F220B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E35039C8-1F98-48EB-B7D5-08E33DF061A7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3382BCB7-657E-4E7B-A2B9-D65AA4DA073B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D19EA72C-E7AE-4A7B-924A-E7550901A49C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
8
nuget.config
Normal file
8
nuget.config
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="NuGet.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="CommandLineNightly" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -9,7 +9,7 @@
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
|
||||
<LangVersion>9</LangVersion>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<AssemblyOriginatorKeyFile>..\..\Squirrel.snk</AssemblyOriginatorKeyFile>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Squirrel.CommandLine.Commands
|
||||
{
|
||||
public class BaseCommand : Command
|
||||
{
|
||||
public RID TargetRuntime { get; set; }
|
||||
|
||||
public string ReleaseDirectory { get; private set; }
|
||||
|
||||
protected Option<DirectoryInfo> ReleaseDirectoryOption { get; private set; }
|
||||
|
||||
//protected static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(BaseCommand));
|
||||
private Dictionary<Option, Action<ParseResult>> _setters = new();
|
||||
|
||||
protected BaseCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
ReleaseDirectoryOption = AddOption<DirectoryInfo>((v) => ReleaseDirectory = v.ToFullNameOrNull(), "-o", "--outputDir")
|
||||
.SetDescription("Output directory for Squirrel packages.")
|
||||
.SetArgumentHelpName("DIR")
|
||||
.SetDefault(new DirectoryInfo(".\\Releases"));
|
||||
}
|
||||
|
||||
public DirectoryInfo GetReleaseDirectory()
|
||||
{
|
||||
var di = new DirectoryInfo(ReleaseDirectory);
|
||||
if (!di.Exists) di.Create();
|
||||
return di;
|
||||
}
|
||||
|
||||
protected virtual Option<T> AddOption<T>(Action<T> setValue, params string[] aliases)
|
||||
{
|
||||
return AddOption(setValue, new Option<T>(aliases));
|
||||
}
|
||||
|
||||
protected virtual Option<T> AddOption<T>(Action<T> setValue, Option<T> opt)
|
||||
{
|
||||
_setters[opt] = (ctx) => setValue(ctx.GetValueForOption(opt));
|
||||
Add(opt);
|
||||
return opt;
|
||||
}
|
||||
|
||||
public virtual void SetProperties(ParseResult context)
|
||||
{
|
||||
foreach (var kvp in _setters) {
|
||||
if (context.Errors.Any(e => e.SymbolResult?.Symbol?.Equals(kvp.Key) == true)) {
|
||||
continue; // skip setting values for options with errors
|
||||
}
|
||||
kvp.Value(context);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual ParseResult ParseAndApply(string command)
|
||||
{
|
||||
var x = this.Parse(command);
|
||||
SetProperties(x);
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
public interface INugetPackCommand
|
||||
{
|
||||
string PackId { get; }
|
||||
string PackVersion { get; }
|
||||
string PackDirectory { get; }
|
||||
string PackAuthors { get; }
|
||||
string PackTitle { get; }
|
||||
bool IncludePdb { get; }
|
||||
string ReleaseNotes { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Squirrel.CommandLine.Commands
|
||||
{
|
||||
public class GitHubBaseCommand : BaseCommand
|
||||
{
|
||||
public string RepoUrl { get; private set; }
|
||||
|
||||
public string Token { get; private set; }
|
||||
|
||||
protected GitHubBaseCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
AddOption<Uri>((v) => RepoUrl = v.ToAbsoluteOrNull(), "--repoUrl")
|
||||
.SetDescription("Full url to the github repository (eg. 'https://github.com/myname/myrepo').")
|
||||
.SetRequired()
|
||||
.MustBeValidHttpUri();
|
||||
|
||||
AddOption<string>((v) => Token = v, "--token")
|
||||
.SetDescription("OAuth token to use as login credentials.");
|
||||
}
|
||||
}
|
||||
|
||||
public class GitHubDownloadCommand : GitHubBaseCommand
|
||||
{
|
||||
public bool Pre { get; private set; }
|
||||
|
||||
public GitHubDownloadCommand()
|
||||
: base("github", "Download latest release from GitHub repository.")
|
||||
{
|
||||
AddOption<bool>((v) => Pre = v, "--pre")
|
||||
.SetDescription("Get latest pre-release instead of stable.");
|
||||
}
|
||||
}
|
||||
|
||||
public class GitHubUploadCommand : GitHubBaseCommand
|
||||
{
|
||||
public bool Publish { get; private set; }
|
||||
|
||||
public string ReleaseName { get; private set; }
|
||||
|
||||
public GitHubUploadCommand()
|
||||
: base("github", "Upload releases to a GitHub repository.")
|
||||
{
|
||||
AddOption<bool>((v) => Publish = v, "--publish")
|
||||
.SetDescription("Publish release instead of creating draft.");
|
||||
|
||||
AddOption<string>((v) => ReleaseName = v, "--releaseName")
|
||||
.SetDescription("A custom name for created release.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
|
||||
ReleaseDirectoryOption.SetRequired();
|
||||
ReleaseDirectoryOption.MustNotBeEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Squirrel.CommandLine.Commands
|
||||
{
|
||||
public class HttpDownloadCommand : BaseCommand
|
||||
{
|
||||
public string Url { get; private set; }
|
||||
|
||||
public HttpDownloadCommand()
|
||||
: base("http", "Download latest release from a HTTP source.")
|
||||
{
|
||||
AddOption<Uri>((v) => Url = v.ToAbsoluteOrNull(), "--url")
|
||||
.SetDescription("Url to download remote releases from.")
|
||||
.MustBeValidHttpUri()
|
||||
.SetRequired();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
using System.CommandLine;
|
||||
using System.IO;
|
||||
|
||||
namespace Squirrel.CommandLine.Commands
|
||||
{
|
||||
public class BundleOsxCommand : BaseCommand
|
||||
{
|
||||
public string PackId { get; private set; }
|
||||
|
||||
public string PackVersion { get; private set; }
|
||||
|
||||
public string PackDirectory { get; private set; }
|
||||
|
||||
public string PackAuthors { get; private set; }
|
||||
|
||||
public string PackTitle { get; private set; }
|
||||
|
||||
public string EntryExecutableName { get; private set; }
|
||||
|
||||
public string Icon { get; private set; }
|
||||
|
||||
public string BundleId { get; private set; }
|
||||
|
||||
public BundleOsxCommand()
|
||||
: base("bundle", "Create's an OSX .app bundle from a folder containing application files.")
|
||||
{
|
||||
AddOption<string>((v) => PackId = v, "--packId", "-u")
|
||||
.SetDescription("Unique Squirrel Id for application bundle.")
|
||||
.SetArgumentHelpName("ID")
|
||||
.SetRequired()
|
||||
.RequiresValidNuGetId();
|
||||
|
||||
// TODO add parser straight to SemanticVersion?
|
||||
AddOption<string>((v) => PackVersion = v, "--packVersion", "-v")
|
||||
.SetDescription("Current version for application bundle.")
|
||||
.SetArgumentHelpName("VERSION")
|
||||
.SetRequired()
|
||||
.RequiresSemverCompliant();
|
||||
|
||||
AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
|
||||
.SetDescription("Directory containing application files for release.")
|
||||
.SetArgumentHelpName("DIR")
|
||||
.SetRequired()
|
||||
.MustNotBeEmpty();
|
||||
|
||||
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
|
||||
.SetDescription("Company name or comma-delimited list of authors.")
|
||||
.SetArgumentHelpName("AUTHORS");
|
||||
|
||||
AddOption<string>((v) => PackTitle = v, "--packTitle")
|
||||
.SetDescription("Display/friendly name for application.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
|
||||
AddOption<string>((v) => EntryExecutableName = v, "-e", "--mainExe")
|
||||
.SetDescription("The file name of the main/entry executable.")
|
||||
.SetArgumentHelpName("NAME")
|
||||
.SetRequired();
|
||||
|
||||
AddOption<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
|
||||
.SetDescription("Path to the .icns file for this bundle.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly()
|
||||
.SetRequired()
|
||||
.RequiresExtension(".icns");
|
||||
|
||||
AddOption<string>((v) => BundleId = v, "--bundleId")
|
||||
.SetDescription("Optional Apple bundle Id.")
|
||||
.SetArgumentHelpName("ID");
|
||||
}
|
||||
}
|
||||
|
||||
public class ReleasifyOsxCommand : BaseCommand
|
||||
{
|
||||
public string BundleDirectory { get; private set; }
|
||||
|
||||
public bool IncludePdb { get; private set; }
|
||||
|
||||
public string ReleaseNotes { get; private set; }
|
||||
|
||||
public bool NoDelta { get; private set; }
|
||||
|
||||
public bool NoPackage { get; private set; }
|
||||
|
||||
public string PackageWelcome { get; private set; }
|
||||
|
||||
public string PackageReadme { get; private set; }
|
||||
|
||||
public string PackageLicense { get; private set; }
|
||||
|
||||
public string PackageConclusion { get; private set; }
|
||||
|
||||
public string SigningAppIdentity { get; private set; }
|
||||
|
||||
public string SigningInstallIdentity { get; private set; }
|
||||
|
||||
public string SigningEntitlements { get; private set; }
|
||||
|
||||
public string NotaryProfile { get; private set; }
|
||||
|
||||
public ReleasifyOsxCommand()
|
||||
: base("releasify", "Converts an application bundle into a Squirrel release and installer.")
|
||||
{
|
||||
AddOption<DirectoryInfo>((v) => BundleDirectory = v.ToFullNameOrNull(), "-b", "--bundle")
|
||||
.SetDescription("The bundle to convert into a Squirrel release.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.MustNotBeEmpty()
|
||||
.RequiresExtension(".app")
|
||||
.SetRequired();
|
||||
|
||||
AddOption<bool>((v) => IncludePdb = v, "--includePdb")
|
||||
.SetDescription("Add *.pdb files to release package.");
|
||||
|
||||
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
|
||||
.SetDescription("File with markdown-formatted notes for this version.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly();
|
||||
|
||||
AddOption<bool>((v) => NoDelta = v, "--noDelta")
|
||||
.SetDescription("Skip the generation of delta packages.");
|
||||
|
||||
if (SquirrelRuntimeInfo.IsOSX) {
|
||||
AddOption<bool>((v) => NoPackage = v, "--noPkg")
|
||||
.SetDescription("Skip generating a .pkg installer.");
|
||||
|
||||
AddOption<FileInfo>((v) => PackageWelcome = v.ToFullNameOrNull(), "--pkgWelcome")
|
||||
.SetDescription("Set the installer package welcome content.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly();
|
||||
|
||||
AddOption<FileInfo>((v) => PackageReadme = v.ToFullNameOrNull(), "--pkgReadme")
|
||||
.SetDescription("Set the installer package readme content.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly();
|
||||
|
||||
AddOption<FileInfo>((v) => PackageLicense = v.ToFullNameOrNull(), "--pkgLicense")
|
||||
.SetDescription("Set the installer package license content.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly();
|
||||
|
||||
AddOption<FileInfo>((v) => PackageConclusion = v.ToFullNameOrNull(), "--pkgConclusion")
|
||||
.SetDescription("Set the installer package conclusion content.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly();
|
||||
|
||||
AddOption<string>((v) => SigningAppIdentity = v, "--signAppIdentity")
|
||||
.SetDescription("The subject name of the cert to use for app code signing.")
|
||||
.SetArgumentHelpName("SUBJECT");
|
||||
|
||||
AddOption<string>((v) => SigningInstallIdentity = v, "--signInstallIdentity")
|
||||
.SetDescription("The subject name of the cert to use for installation packages.")
|
||||
.SetArgumentHelpName("SUBJECT");
|
||||
|
||||
AddOption<FileInfo>((v) => SigningEntitlements = v.ToFullNameOrNull(), "--signEntitlements")
|
||||
.SetDescription("Path to entitlements file for hardened runtime signing.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly()
|
||||
.RequiresExtension(".entitlements");
|
||||
|
||||
AddOption<string>((v) => NotaryProfile = v, "--notaryProfile")
|
||||
.SetDescription("Name of profile containing Apple credentials stored with notarytool.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
using System;
|
||||
using System.CommandLine.Parsing;
|
||||
|
||||
namespace Squirrel.CommandLine.Commands
|
||||
{
|
||||
public class S3BaseCommand : BaseCommand
|
||||
{
|
||||
public string KeyId { get; private set; }
|
||||
|
||||
public string Secret { get; private set; }
|
||||
|
||||
public string Region { get; private set; }
|
||||
|
||||
public string Endpoint { get; private set; }
|
||||
|
||||
public string Bucket { get; private set; }
|
||||
|
||||
public string PathPrefix { get; private set; }
|
||||
|
||||
protected S3BaseCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
AddOption<string>((v) => KeyId = v, "--keyId")
|
||||
.SetDescription("Authentication identifier or access key.")
|
||||
.SetArgumentHelpName("KEYID")
|
||||
.SetRequired();
|
||||
|
||||
AddOption<string>((v) => Secret = v, "--secret")
|
||||
.SetDescription("Authentication secret key.")
|
||||
.SetArgumentHelpName("KEY")
|
||||
.SetRequired();
|
||||
|
||||
var region = AddOption<string>((v) => Region = v, "--region")
|
||||
.SetDescription("AWS service region (eg. us-west-1).")
|
||||
.SetArgumentHelpName("REGION");
|
||||
|
||||
region.AddValidator(MustBeValidAwsRegion);
|
||||
|
||||
var endpoint = AddOption<Uri>((v) => Endpoint = v.ToAbsoluteOrNull(), "--endpoint")
|
||||
.SetDescription("Custom service url (backblaze, digital ocean, etc).")
|
||||
.SetArgumentHelpName("URL")
|
||||
.MustBeValidHttpUri();
|
||||
|
||||
this.AreMutuallyExclusive(region, endpoint);
|
||||
this.AtLeastOneRequired(region, endpoint);
|
||||
|
||||
AddOption<string>((v) => Bucket = v, "--bucket")
|
||||
.SetDescription("Name of the S3 bucket.")
|
||||
.SetArgumentHelpName("NAME")
|
||||
.SetRequired();
|
||||
|
||||
AddOption<string>((v) => PathPrefix = v, "--pathPrefix")
|
||||
.SetDescription("A sub-folder used for files in the bucket, for creating release channels (eg. 'stable' or 'dev').")
|
||||
.SetArgumentHelpName("PREFIX");
|
||||
}
|
||||
|
||||
private static void MustBeValidAwsRegion(OptionResult result)
|
||||
{
|
||||
for (var i = 0; i < result.Tokens.Count; i++) {
|
||||
var region = result.Tokens[i].Value;
|
||||
if (!string.IsNullOrWhiteSpace(region)) {
|
||||
var r = Amazon.RegionEndpoint.GetBySystemName(result.Tokens[0].Value);
|
||||
if (r is null || r.DisplayName == "Unknown") {
|
||||
result.ErrorMessage = $"Region '{region}' lookup failed, is this a valid AWS region?";
|
||||
}
|
||||
} else {
|
||||
result.ErrorMessage = "A region value is required";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class S3DownloadCommand : S3BaseCommand
|
||||
{
|
||||
public S3DownloadCommand()
|
||||
: base("s3", "Download latest release from an S3 bucket.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class S3UploadCommand : S3BaseCommand
|
||||
{
|
||||
public bool Overwrite { get; private set; }
|
||||
|
||||
public int KeepMaxReleases { get; private set; }
|
||||
|
||||
public S3UploadCommand()
|
||||
: base("s3", "Upload releases to a S3 bucket.")
|
||||
{
|
||||
AddOption<bool>((v) => Overwrite = v, "--overwrite")
|
||||
.SetDescription("Replace remote files if local files have changed.");
|
||||
|
||||
AddOption<int>((v) => KeepMaxReleases = v, "--keepMaxReleases")
|
||||
.SetDescription("Apply a retention policy which keeps only the specified number of old versions in remote source.")
|
||||
.SetArgumentHelpName("NUMBER");
|
||||
|
||||
ReleaseDirectoryOption.SetRequired();
|
||||
ReleaseDirectoryOption.MustNotBeEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.IO;
|
||||
|
||||
namespace Squirrel.CommandLine.Commands
|
||||
{
|
||||
public class SigningCommand : BaseCommand
|
||||
{
|
||||
public string SignParameters { get; private set; }
|
||||
|
||||
public bool SignSkipDll { get; private set; }
|
||||
|
||||
public int SignParallel { get; private set; }
|
||||
|
||||
public string SignTemplate { get; private set; }
|
||||
|
||||
protected SigningCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
var signTemplate = AddOption<string>((v) => SignTemplate = v, "--signTemplate")
|
||||
.SetDescription("Use a custom signing command. {{file}} will be replaced by the path to sign.")
|
||||
.SetArgumentHelpName("COMMAND")
|
||||
.MustContain("{{file}}");
|
||||
|
||||
AddOption<bool>((v) => SignSkipDll = v, "--signSkipDll")
|
||||
.SetDescription("Only signs EXE files, and skips signing DLL files.");
|
||||
|
||||
if (SquirrelRuntimeInfo.IsWindows) {
|
||||
var signParams = AddOption<string>((v) => SignParameters = v, "--signParams", "-n")
|
||||
.SetDescription("Sign files via signtool.exe using these parameters.")
|
||||
.SetArgumentHelpName("PARAMS");
|
||||
|
||||
this.AreMutuallyExclusive(signTemplate, signParams);
|
||||
|
||||
AddOption<int>((v) => SignParallel = v, "--signParallel")
|
||||
.SetDescription("The number of files to sign in each call to signtool.exe.")
|
||||
.SetArgumentHelpName("NUM")
|
||||
.MustBeBetween(1, 1000)
|
||||
.SetDefaultValue(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ReleasifyWindowsCommand : SigningCommand
|
||||
{
|
||||
public string Package { get; set; }
|
||||
|
||||
public string BaseUrl { get; private set; }
|
||||
|
||||
public string DebugSetupExe { get; private set; }
|
||||
|
||||
public bool NoDelta { get; private set; }
|
||||
|
||||
public string Runtimes { get; private set; }
|
||||
|
||||
public string SplashImage { get; private set; }
|
||||
|
||||
public string Icon { get; private set; }
|
||||
|
||||
public string[] SquirrelAwareExecutableNames { get; private set; }
|
||||
|
||||
public string AppIcon { get; private set; }
|
||||
|
||||
public bool BuildMsi { get; private set; }
|
||||
|
||||
public string MsiVersion { get; private set; }
|
||||
|
||||
public ReleasifyWindowsCommand()
|
||||
: this("releasify", "Take an existing nuget package and convert it into a Squirrel release.")
|
||||
{
|
||||
AddOption<FileInfo>((v) => Package = v.ToFullNameOrNull(), "-p", "--package")
|
||||
.SetDescription("Path to a '.nupkg' package to releasify.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.SetRequired()
|
||||
.ExistingOnly()
|
||||
.RequiresExtension(".nupkg");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This constructor is used by the pack command, which requires all the same properties but
|
||||
/// does not allow the user to provide the Package (it is created/populated by Squirrel).
|
||||
/// </summary>
|
||||
protected ReleasifyWindowsCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
AddOption<Uri>((v) => BaseUrl = v.ToAbsoluteOrNull(), "-b", "--baseUrl")
|
||||
.SetDescription("Provides a base URL to prefix the RELEASES file packages with.")
|
||||
.SetHidden()
|
||||
.MustBeValidHttpUri();
|
||||
|
||||
AddOption<FileInfo>((v) => DebugSetupExe = v.ToFullNameOrNull(), "--debugSetupExe")
|
||||
.SetDescription("Uses the Setup.exe at this {PATH} to create the bundle, and then replaces it with the bundle. " +
|
||||
"Used for locally debugging Setup.exe with a real bundle attached.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.SetHidden()
|
||||
.ExistingOnly()
|
||||
.RequiresExtension(".exe");
|
||||
|
||||
AddOption<bool>((v) => NoDelta = v, "--noDelta")
|
||||
.SetDescription("Skip the generation of delta packages.");
|
||||
|
||||
AddOption<string>((v) => Runtimes = v, "-f", "--framework")
|
||||
.SetDescription("List of required runtimes to install during setup. example: 'net6,vcredist143'.")
|
||||
.SetArgumentHelpName("RUNTIMES")
|
||||
.MustBeValidFrameworkString();
|
||||
|
||||
AddOption<FileInfo>((v) => SplashImage = v.ToFullNameOrNull(), "-s", "--splashImage")
|
||||
.SetDescription("Path to image displayed during installation.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly();
|
||||
|
||||
AddOption<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
|
||||
.SetDescription("Path to .ico for Setup.exe and Update.exe.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly()
|
||||
.RequiresExtension(".ico");
|
||||
|
||||
AddOption<string[]>((v) => SquirrelAwareExecutableNames = v ?? new string[0], "-e", "--mainExe")
|
||||
.SetDescription("Name of one or more SquirrelAware executables.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
|
||||
AddOption<FileInfo>((v) => AppIcon = v.ToFullNameOrNull(), "--appIcon")
|
||||
.SetDescription("Path to .ico for 'Apps and Features' list.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly()
|
||||
.RequiresExtension(".ico");
|
||||
|
||||
if (SquirrelRuntimeInfo.IsWindows) {
|
||||
AddOption<bool>((v) => BuildMsi = v, "--msi")
|
||||
.SetDescription("Compile a .msi machine-wide deployment tool.")
|
||||
.SetArgumentHelpName("BITNESS");
|
||||
|
||||
AddOption<string>((v) => MsiVersion = v, "--msiVersion")
|
||||
.SetDescription("Override the product version for the generated msi.")
|
||||
.SetArgumentHelpName("VERSION")
|
||||
.MustBeValidMsiVersion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PackWindowsCommand : ReleasifyWindowsCommand, INugetPackCommand
|
||||
{
|
||||
public string PackId { get; private set; }
|
||||
|
||||
public string PackVersion { get; private set; }
|
||||
|
||||
public string PackDirectory { get; private set; }
|
||||
|
||||
public string PackAuthors { get; private set; }
|
||||
|
||||
public string PackTitle { get; private set; }
|
||||
|
||||
public bool IncludePdb { get; private set; }
|
||||
|
||||
public string ReleaseNotes { get; private set; }
|
||||
|
||||
public PackWindowsCommand()
|
||||
: base("pack", "Creates a Squirrel release from a folder containing application files.")
|
||||
{
|
||||
AddOption<string>((v) => PackId = v, "--packId", "-u")
|
||||
.SetDescription("Unique Id for application bundle.")
|
||||
.SetArgumentHelpName("ID")
|
||||
.SetRequired()
|
||||
.RequiresValidNuGetId();
|
||||
|
||||
// TODO add parser straight to SemanticVersion
|
||||
AddOption<string>((v) => PackVersion = v, "--packVersion", "-v")
|
||||
.SetDescription("Current version for application bundle.")
|
||||
.SetArgumentHelpName("VERSION")
|
||||
.SetRequired()
|
||||
.RequiresSemverCompliant();
|
||||
|
||||
AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
|
||||
.SetDescription("Directory containing application files for release.")
|
||||
.SetArgumentHelpName("DIR")
|
||||
.SetRequired()
|
||||
.MustNotBeEmpty();
|
||||
|
||||
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
|
||||
.SetDescription("Company name or comma-delimited list of authors.")
|
||||
.SetArgumentHelpName("AUTHORS");
|
||||
|
||||
AddOption<string>((v) => PackTitle = v, "--packTitle")
|
||||
.SetDescription("Display/friendly name for application.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
|
||||
AddOption<bool>((v) => IncludePdb = v, "--includePdb")
|
||||
.SetDescription("Add *.pdb files to release package");
|
||||
|
||||
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
|
||||
.SetDescription("File with markdown-formatted notes for this version.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.ExistingOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel.SimpleSplat;
|
||||
using NugetLevel = NuGet.Common.LogLevel;
|
||||
using NugetLogger = NuGet.Common.ILogger;
|
||||
using NugetMessage = NuGet.Common.ILogMessage;
|
||||
|
||||
namespace Squirrel.CommandLine
|
||||
{
|
||||
class ConsoleLogger : ILogger, NugetLogger
|
||||
{
|
||||
public LogLevel Level { get; set; } = LogLevel.Info;
|
||||
|
||||
private readonly object gate = new object();
|
||||
|
||||
private readonly string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData).TrimEnd('/', '\\');
|
||||
private readonly string localTemp = Path.GetTempPath().TrimEnd('/', '\\');
|
||||
|
||||
public void Write(string message, LogLevel logLevel)
|
||||
{
|
||||
if (logLevel < Level) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SquirrelRuntimeInfo.IsWindows) {
|
||||
message = message.Replace(localTemp, "%temp%", StringComparison.InvariantCultureIgnoreCase);
|
||||
message = message.Replace(localAppData, "%localappdata%", StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
lock (gate) {
|
||||
string lvl = logLevel.ToString().Substring(0, 4).ToUpper();
|
||||
if (logLevel == LogLevel.Error || logLevel == LogLevel.Fatal) {
|
||||
Utility.ConsoleWriteWithColor($"[{lvl}] {message}{Environment.NewLine}", ConsoleColor.Red);
|
||||
} else if (logLevel == LogLevel.Warn) {
|
||||
Utility.ConsoleWriteWithColor($"[{lvl}] {message}{Environment.NewLine}", ConsoleColor.Yellow);
|
||||
} else {
|
||||
Console.WriteLine($"[{lvl}] {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ConsoleLogger RegisterLogger()
|
||||
{
|
||||
var logger = new ConsoleLogger();
|
||||
SquirrelLocator.CurrentMutable.Register(() => logger, typeof(ILogger));
|
||||
SquirrelLocator.CurrentMutable.Register(() => logger, typeof(NugetLogger));
|
||||
return logger;
|
||||
}
|
||||
|
||||
#region NuGet.Common.ILogger
|
||||
|
||||
void NugetLogger.LogDebug(string data)
|
||||
{
|
||||
Write(data, LogLevel.Debug);
|
||||
}
|
||||
|
||||
void NugetLogger.LogVerbose(string data)
|
||||
{
|
||||
Write(data, LogLevel.Debug);
|
||||
}
|
||||
|
||||
void NugetLogger.LogInformation(string data)
|
||||
{
|
||||
Write(data, LogLevel.Info);
|
||||
}
|
||||
|
||||
void NugetLogger.LogMinimal(string data)
|
||||
{
|
||||
Write(data, LogLevel.Info);
|
||||
}
|
||||
|
||||
void NugetLogger.LogWarning(string data)
|
||||
{
|
||||
Write(data, LogLevel.Warn);
|
||||
}
|
||||
|
||||
void NugetLogger.LogError(string data)
|
||||
{
|
||||
Write(data, LogLevel.Error);
|
||||
}
|
||||
|
||||
void NugetLogger.LogInformationSummary(string data)
|
||||
{
|
||||
Write(data, LogLevel.Info);
|
||||
}
|
||||
|
||||
LogLevel NugetToLogLevel(NugetLevel level)
|
||||
{
|
||||
return level switch {
|
||||
NugetLevel.Debug => LogLevel.Debug,
|
||||
NugetLevel.Verbose => LogLevel.Debug,
|
||||
NugetLevel.Information => LogLevel.Info,
|
||||
NugetLevel.Minimal => LogLevel.Info,
|
||||
NugetLevel.Warning => LogLevel.Warn,
|
||||
NugetLevel.Error => LogLevel.Error,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
|
||||
};
|
||||
}
|
||||
|
||||
void NugetLogger.Log(NugetLevel level, string data)
|
||||
{
|
||||
Write(data, NugetToLogLevel(level));
|
||||
}
|
||||
|
||||
Task NugetLogger.LogAsync(NugetLevel level, string data)
|
||||
{
|
||||
Write(data, NugetToLogLevel(level));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void NugetLogger.Log(NugetMessage message)
|
||||
{
|
||||
Write(message.Message, NugetToLogLevel(message.Level));
|
||||
}
|
||||
|
||||
Task NugetLogger.LogAsync(NugetMessage message)
|
||||
{
|
||||
Write(message.Message, NugetToLogLevel(message.Level));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: InternalsVisibleTo("Squirrel.Tests, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Squirrel, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("SquirrelMac, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine.OSX, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine.Windows, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Update, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("UpdateMac, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Update.Windows, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Update.OSX, PublicKey=" + SNK.SHA1)]
|
||||
@@ -1,158 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Octokit;
|
||||
using Squirrel.CommandLine.Commands;
|
||||
using Squirrel.SimpleSplat;
|
||||
using Squirrel.Sources;
|
||||
|
||||
namespace Squirrel.CommandLine.Sync
|
||||
{
|
||||
internal static class GitHubRepository
|
||||
{
|
||||
internal readonly static IFullLogger Log = SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(GitHubRepository));
|
||||
|
||||
public static async Task DownloadRecentPackages(GitHubDownloadCommand options)
|
||||
{
|
||||
var releaseDirectoryInfo = options.GetReleaseDirectory();
|
||||
|
||||
if (String.IsNullOrWhiteSpace(options.Token))
|
||||
Log.Warn("No GitHub access token provided. Unauthenticated requests will be limited to 60 per hour.");
|
||||
|
||||
Log.Info("Fetching RELEASES...");
|
||||
var source = new GithubSource(options.RepoUrl, options.Token, options.Pre);
|
||||
var latestReleaseEntries = await source.GetReleaseFeed();
|
||||
|
||||
if (latestReleaseEntries == null || latestReleaseEntries.Length == 0) {
|
||||
Log.Warn("No github release or assets found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Info($"Found {latestReleaseEntries.Length} assets in RELEASES file for GitHub version {source.Release.Name}.");
|
||||
|
||||
var releasesToDownload = latestReleaseEntries
|
||||
.Where(x => !x.IsDelta)
|
||||
.OrderByDescending(x => x.Version)
|
||||
.Take(1)
|
||||
.Select(x => new {
|
||||
Obj = x,
|
||||
LocalPath = Path.Combine(releaseDirectoryInfo.FullName, x.Filename),
|
||||
Filename = x.Filename,
|
||||
});
|
||||
|
||||
foreach (var entry in releasesToDownload) {
|
||||
if (File.Exists(entry.LocalPath)) {
|
||||
Log.Warn($"File '{entry.Filename}' exists on disk, skipping download.");
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.Info($"Downloading {entry.Filename}...");
|
||||
await source.DownloadReleaseEntry(entry.Obj, entry.LocalPath, (p) => { });
|
||||
}
|
||||
|
||||
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
||||
Log.Info("Done.");
|
||||
}
|
||||
|
||||
public static async Task UploadMissingPackages(GitHubUploadCommand options)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(options.Token))
|
||||
throw new InvalidOperationException("Must provide access token to create a GitHub release.");
|
||||
|
||||
var releaseDirectoryInfo = options.GetReleaseDirectory();
|
||||
|
||||
var repoUri = new Uri(options.RepoUrl);
|
||||
var repoParts = repoUri.AbsolutePath.Trim('/').Split('/');
|
||||
if (repoParts.Length != 2)
|
||||
throw new Exception($"Invalid GitHub URL, '{repoUri.AbsolutePath}' should be in the format 'owner/repo'");
|
||||
|
||||
var repoOwner = repoParts[0];
|
||||
var repoName = repoParts[1];
|
||||
|
||||
var client = new GitHubClient(new ProductHeaderValue("Clowd.Squirrel")) {
|
||||
Credentials = new Credentials(options.Token)
|
||||
};
|
||||
|
||||
var releasesPath = Path.Combine(releaseDirectoryInfo.FullName, "RELEASES");
|
||||
if (!File.Exists(releasesPath))
|
||||
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
||||
|
||||
var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath)).ToArray();
|
||||
if (releases.Length == 0)
|
||||
throw new Exception("There are no nupkg's in the releases directory to upload");
|
||||
|
||||
var ver = Enumerable.MaxBy(releases, x => x.Version);
|
||||
if (ver == null)
|
||||
throw new Exception("There are no nupkg's in the releases directory to upload");
|
||||
var semVer = ver.Version;
|
||||
|
||||
Log.Info($"Preparing to upload latest local release to GitHub");
|
||||
|
||||
var newReleaseReq = new NewRelease(semVer.ToString()) {
|
||||
Body = ver.GetReleaseNotes(releaseDirectoryInfo.FullName, ReleaseNotesFormat.Markdown),
|
||||
Draft = true,
|
||||
Prerelease = semVer.HasMetadata || semVer.IsPrerelease,
|
||||
Name = string.IsNullOrWhiteSpace(options.ReleaseName)
|
||||
? semVer.ToString()
|
||||
: options.ReleaseName,
|
||||
};
|
||||
|
||||
Log.Info($"Creating draft release titled '{semVer.ToString()}'");
|
||||
|
||||
var existingReleases = await client.Repository.Release.GetAll(repoOwner, repoName);
|
||||
if (existingReleases.Any(r => r.TagName == semVer.ToString())) {
|
||||
throw new Exception($"There is already an existing release tagged '{semVer}'. Please delete this release or choose a new version number.");
|
||||
}
|
||||
|
||||
var release = await client.Repository.Release.Create(repoOwner, repoName, newReleaseReq);
|
||||
|
||||
// locate files to upload
|
||||
var files = releaseDirectoryInfo.GetFiles("*", SearchOption.TopDirectoryOnly);
|
||||
var msiFile = files.SingleOrDefault(f => f.FullName.EndsWith(".msi", StringComparison.InvariantCultureIgnoreCase));
|
||||
var setupFile = files.Where(f => f.FullName.EndsWith("Setup.exe", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ContextualSingle("release directory", "Setup.exe file");
|
||||
|
||||
var releasesToUpload = releases.Where(x => x.Version == semVer).ToArray();
|
||||
MemoryStream releasesFileToUpload = new MemoryStream();
|
||||
ReleaseEntry.WriteReleaseFile(releasesToUpload, releasesFileToUpload);
|
||||
var releasesBytes = releasesFileToUpload.ToArray();
|
||||
|
||||
// upload nupkg's
|
||||
foreach (var r in releasesToUpload) {
|
||||
var path = Path.Combine(releaseDirectoryInfo.FullName, r.Filename);
|
||||
await UploadFileAsAsset(client, release, path);
|
||||
}
|
||||
|
||||
// other files
|
||||
await UploadFileAsAsset(client, release, setupFile.FullName);
|
||||
if (msiFile != null) await UploadFileAsAsset(client, release, msiFile.FullName);
|
||||
|
||||
// RELEASES
|
||||
Log.Info($"Uploading RELEASES");
|
||||
var data = new ReleaseAssetUpload("RELEASES", "application/octet-stream", new MemoryStream(releasesBytes), TimeSpan.FromMinutes(1));
|
||||
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
||||
|
||||
Log.Info($"Done creating draft GitHub release.");
|
||||
|
||||
// convert draft to full release
|
||||
if (options.Publish) {
|
||||
Log.Info("Converting draft to full published release.");
|
||||
var upd = release.ToUpdate();
|
||||
upd.Draft = false;
|
||||
release = await client.Repository.Release.Edit(repoOwner, repoName, release.Id, upd);
|
||||
}
|
||||
|
||||
Log.Info("Release URL: " + release.HtmlUrl);
|
||||
}
|
||||
|
||||
private static async Task UploadFileAsAsset(GitHubClient client, Release release, string filePath)
|
||||
{
|
||||
Log.Info($"Uploading asset '{Path.GetFileName(filePath)}'");
|
||||
using var stream = File.OpenRead(filePath);
|
||||
var data = new ReleaseAssetUpload(Path.GetFileName(filePath), "application/octet-stream", stream, TimeSpan.FromMinutes(30));
|
||||
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NuGet.Versioning;
|
||||
using Squirrel.NuGet;
|
||||
|
||||
namespace Squirrel.CommandLine
|
||||
{
|
||||
internal static class SystemCommandLineExtensions
|
||||
{
|
||||
public static string ToFullNameOrNull(this FileSystemInfo fsi)
|
||||
{
|
||||
return fsi?.FullName;
|
||||
}
|
||||
|
||||
public static string ToAbsoluteOrNull(this Uri uri)
|
||||
{
|
||||
if (uri?.IsAbsoluteUri == true) return uri.AbsoluteUri;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Option<T> SetDescription<T>(this Option<T> option, string description)
|
||||
{
|
||||
option.Description = description;
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<T> SetHidden<T>(this Option<T> option, bool isHidden = true)
|
||||
{
|
||||
option.IsHidden = isHidden;
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<T> SetRequired<T>(this Option<T> option, bool isRequired = true)
|
||||
{
|
||||
option.IsRequired = isRequired;
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<T> SetDefault<T>(this Option<T> option, T defaultValue)
|
||||
{
|
||||
option.SetDefaultValue(defaultValue);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<T> SetArgumentHelpName<T>(this Option<T> option, string argumentHelpName)
|
||||
{
|
||||
option.ArgumentHelpName = argumentHelpName;
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<int> MustBeBetween(this Option<int> option, int minimum, int maximum)
|
||||
{
|
||||
option.AddValidator(x => Validate.MustBeBetween(x, minimum, maximum));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<Uri> MustBeValidHttpUri(this Option<Uri> option)
|
||||
{
|
||||
option.RequiresScheme(Uri.UriSchemeHttp, Uri.UriSchemeHttps).RequiresAbsolute();
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<FileInfo> RequiresExtension(this Option<FileInfo> option, string extension)
|
||||
{
|
||||
option.AddValidator(x => Validate.RequiresExtension(x, extension));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<DirectoryInfo> RequiresExtension(this Option<DirectoryInfo> option, string extension)
|
||||
{
|
||||
option.AddValidator(x => Validate.RequiresExtension(x, extension));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Command AreMutuallyExclusive(this Command command, params Option[] options)
|
||||
{
|
||||
command.AddValidator(x => Validate.AreMutuallyExclusive(x, options));
|
||||
return command;
|
||||
}
|
||||
|
||||
//public static Command RequiredAllowObsoleteFallback(this Command command, Option option, Option obsoleteOption)
|
||||
//{
|
||||
// command.AddValidator(x => Validate.AtLeastOneRequired(x, new[] { option, obsoleteOption }, true));
|
||||
// return command;
|
||||
//}
|
||||
|
||||
public static Command AtLeastOneRequired(this Command command, params Option[] options)
|
||||
{
|
||||
command.AddValidator(x => Validate.AtLeastOneRequired(x, options, false));
|
||||
return command;
|
||||
}
|
||||
|
||||
public static Option<string> MustContain(this Option<string> option, string value)
|
||||
{
|
||||
option.AddValidator(x => Validate.MustContain(x, value));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<Uri> RequiresScheme(this Option<Uri> option, params string[] validSchemes)
|
||||
{
|
||||
option.AddValidator(x => Validate.RequiresScheme(x, validSchemes));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<Uri> RequiresAbsolute(this Option<Uri> option, params string[] validSchemes)
|
||||
{
|
||||
option.AddValidator(Validate.RequiresAbsolute);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<string> RequiresValidNuGetId(this Option<string> option)
|
||||
{
|
||||
option.AddValidator(Validate.RequiresValidNuGetId);
|
||||
return option;
|
||||
}
|
||||
|
||||
//TODO: Could setup the options to accept type SemanticVersion and apply an appropriate parser for it
|
||||
public static Option<string> RequiresSemverCompliant(this Option<string> option)
|
||||
{
|
||||
option.AddValidator(Validate.RequiresSemverCompliant);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<DirectoryInfo> MustNotBeEmpty(this Option<DirectoryInfo> option)
|
||||
{
|
||||
option.AddValidator(Validate.MustNotBeEmpty);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<string> MustBeValidFrameworkString(this Option<string> option)
|
||||
{
|
||||
option.AddValidator(Validate.MustBeValidFrameworkString);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<string> MustBeValidMsiVersion(this Option<string> option)
|
||||
{
|
||||
option.AddValidator(Validate.MustBeValidMsiVersion);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static Option<string> MustBeSupportedRid(this Option<string> option)
|
||||
{
|
||||
option.AddValidator(Validate.MustBeSupportedRid);
|
||||
return option;
|
||||
}
|
||||
|
||||
private static class Validate
|
||||
{
|
||||
public static void MustBeBetween(OptionResult result, int minimum, int maximum)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (int.TryParse(result.Tokens[i].Value, out int value)) {
|
||||
if (value < minimum || value > maximum) {
|
||||
result.ErrorMessage = $"The value for {result.Token.Value} must be greater than {minimum} and less than {maximum}";
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
result.ErrorMessage = $"{result.Tokens[i].Value} is not a valid integer for {result.Token.Value}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresExtension(OptionResult result, string extension)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (!string.Equals(Path.GetExtension(result.Tokens[i].Value), extension, StringComparison.InvariantCultureIgnoreCase)) {
|
||||
result.ErrorMessage = $"{result.Token.Value} does not have an {extension} extension";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AreMutuallyExclusive(CommandResult result, Option[] options)
|
||||
{
|
||||
var specifiedOptions = options
|
||||
.Where(x => result.FindResultFor(x) is not null)
|
||||
.ToList();
|
||||
if (specifiedOptions.Count > 1) {
|
||||
string optionsString = string.Join(" and ", specifiedOptions.Select(x => $"'{x.Aliases.First()}'"));
|
||||
result.ErrorMessage = $"Cannot use {optionsString} options together, please choose one.";
|
||||
}
|
||||
}
|
||||
|
||||
public static void AtLeastOneRequired(CommandResult result, Option[] options, bool onlyShowFirst = false)
|
||||
{
|
||||
var anySpecifiedOptions = options
|
||||
.Any(x => result.FindResultFor(x) is not null);
|
||||
if (!anySpecifiedOptions) {
|
||||
if (onlyShowFirst) {
|
||||
result.ErrorMessage = $"Required argument missing for option: {options.First().Aliases.First()}";
|
||||
} else {
|
||||
string optionsString = string.Join(" and ", options.Select(x => $"'{x.Aliases.First()}'"));
|
||||
result.ErrorMessage = $"At least one of the following options are required {optionsString}.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustContain(OptionResult result, string value)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (result.Tokens[i].Value?.Contains(value) == false) {
|
||||
result.ErrorMessage = $"{result.Token.Value} must contain '{value}'. Current value is '{result.Tokens[i].Value}'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresScheme(OptionResult result, string[] validSchemes)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (Uri.TryCreate(result.Tokens[i].Value, UriKind.RelativeOrAbsolute, out Uri uri) &&
|
||||
uri.IsAbsoluteUri &&
|
||||
!validSchemes.Contains(uri.Scheme)) {
|
||||
result.ErrorMessage = $"{result.Token.Value} must contain a Uri with one of the following schems: {string.Join(", ", validSchemes)}. Current value is '{result.Tokens[i].Value}'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresAbsolute(OptionResult result)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (!Uri.TryCreate(result.Tokens[i].Value, UriKind.Absolute, out Uri _)) {
|
||||
result.ErrorMessage = $"{result.Token.Value} must contain an absolute Uri. Current value is '{result.Tokens[i].Value}'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresValidNuGetId(OptionResult result)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (!NugetUtil.IsValidNuGetId(result.Tokens[i].Value)) {
|
||||
result.ErrorMessage = $"{result.Token.Value} is an invalid NuGet package id. It must contain only alphanumeric characters, underscores, dashes, and dots.. Current value is '{result.Tokens[i].Value}'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresSemverCompliant(OptionResult result)
|
||||
{
|
||||
string specifiedAlias = result.Token.Value;
|
||||
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
string version = result.Tokens[i].Value;
|
||||
//TODO: This is duplicating NugetUtil.ThrowIfVersionNotSemverCompliant
|
||||
if (SemanticVersion.TryParse(version, out var parsed)) {
|
||||
if (parsed < new SemanticVersion(0, 0, 1)) {
|
||||
result.ErrorMessage = $"{result.Token.Value} contains an invalid package version '{version}', it must be >= 0.0.1.";
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
result.ErrorMessage = $"{result.Token.Value} contains an invalid package version '{version}', it must be a 3-part SemVer2 compliant version string.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustNotBeEmpty(OptionResult result)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
var token = result.Tokens[i];
|
||||
|
||||
if (!Directory.Exists(token.Value) ||
|
||||
!Directory.EnumerateFileSystemEntries(token.Value).Any()) {
|
||||
result.ErrorMessage = $"{result.Token.Value} must be a non-empty directory, but the specified directory '{token.Value}' was empty.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustBeValidFrameworkString(OptionResult result)
|
||||
{
|
||||
for (var i = 0; i < result.Tokens.Count; i++) {
|
||||
var framework = result.Tokens[i].Value;
|
||||
try {
|
||||
Runtimes.ParseDependencyString(framework);
|
||||
} catch (Exception e) {
|
||||
result.ErrorMessage = e.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustBeValidMsiVersion(OptionResult result)
|
||||
{
|
||||
for (var i = 0; i < result.Tokens.Count; i++) {
|
||||
var version = result.Tokens[i].Value;
|
||||
if (Version.TryParse(version, out var parsed)) {
|
||||
if (parsed.Major > 255 || parsed.Minor > 255 || parsed.Build > 65535 || parsed.Revision > 0) {
|
||||
result.ErrorMessage = $"MSI ProductVersion out of bounds '{version}'. Valid range is [0-255].[0-255].[0-65535].[0]";
|
||||
}
|
||||
} else {
|
||||
result.ErrorMessage = "Version string is invalid / could not be parsed.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustBeSupportedRid(OptionResult result)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (!Regex.IsMatch(result.Tokens[i].Value, @"^(?<os>osx|win)\.?(?<ver>[\d\.]+)?(?:-(?<arch>(?:x|arm)\d{2}))$"))
|
||||
result.ErrorMessage = $"Invalid or unsupported runtime '{result.Token.Value}'. Valid example: win-x64, osx-arm64.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/Squirrel.Csq/Commands/BaseCommand.cs
Normal file
69
src/Squirrel.Csq/Commands/BaseCommand.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace Squirrel.Csq.Commands;
|
||||
|
||||
public class BaseCommand : CliCommand
|
||||
{
|
||||
public RID TargetRuntime { get; set; }
|
||||
|
||||
public string ReleaseDirectory { get; private set; }
|
||||
|
||||
protected CliOption<DirectoryInfo> ReleaseDirectoryOption { get; private set; }
|
||||
|
||||
//protected static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(BaseCommand));
|
||||
private Dictionary<CliOption, Action<ParseResult>> _setters = new();
|
||||
|
||||
protected BaseCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
ReleaseDirectoryOption = AddOption<DirectoryInfo>((v) => ReleaseDirectory = v.ToFullNameOrNull(), "-o", "--outputDir")
|
||||
.SetDescription("Output directory for Squirrel packages.")
|
||||
.SetArgumentHelpName("DIR")
|
||||
.SetDefault(new DirectoryInfo(".\\Releases"));
|
||||
}
|
||||
|
||||
public DirectoryInfo GetReleaseDirectory()
|
||||
{
|
||||
var di = new DirectoryInfo(ReleaseDirectory);
|
||||
if (!di.Exists) di.Create();
|
||||
return di;
|
||||
}
|
||||
|
||||
protected virtual CliOption<T> AddOption<T>(Action<T> setValue, params string[] aliases)
|
||||
{
|
||||
return AddOption(setValue, new CliOption<T>(aliases));
|
||||
}
|
||||
|
||||
protected virtual CliOption<T> AddOption<T>(Action<T> setValue, CliOption<T> opt)
|
||||
{
|
||||
_setters[opt] = (ctx) => setValue(ctx.GetValue(opt));
|
||||
Add(opt);
|
||||
return opt;
|
||||
}
|
||||
|
||||
public virtual void SetProperties(ParseResult context)
|
||||
{
|
||||
foreach (var kvp in _setters) {
|
||||
if (context.Errors.Any(e => e.SymbolResult?.Symbol?.Equals(kvp.Key) == true)) {
|
||||
continue; // skip setting values for options with errors
|
||||
}
|
||||
kvp.Value(context);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual ParseResult ParseAndApply(string command)
|
||||
{
|
||||
var x = this.Parse(command);
|
||||
SetProperties(x);
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
public interface INugetPackCommand
|
||||
{
|
||||
string PackId { get; }
|
||||
string PackVersion { get; }
|
||||
string PackDirectory { get; }
|
||||
string PackAuthors { get; }
|
||||
string PackTitle { get; }
|
||||
bool IncludePdb { get; }
|
||||
string ReleaseNotes { get; }
|
||||
}
|
||||
53
src/Squirrel.Csq/Commands/GitHubCommands.cs
Normal file
53
src/Squirrel.Csq/Commands/GitHubCommands.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace Squirrel.Csq.Commands;
|
||||
|
||||
public class GitHubBaseCommand : BaseCommand
|
||||
{
|
||||
public string RepoUrl { get; private set; }
|
||||
|
||||
public string Token { get; private set; }
|
||||
|
||||
protected GitHubBaseCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
AddOption<Uri>((v) => RepoUrl = v.ToAbsoluteOrNull(), "--repoUrl")
|
||||
.SetDescription("Full url to the github repository (eg. 'https://github.com/myname/myrepo').")
|
||||
.SetRequired()
|
||||
.MustBeValidHttpUri();
|
||||
|
||||
AddOption<string>((v) => Token = v, "--token")
|
||||
.SetDescription("OAuth token to use as login credentials.");
|
||||
}
|
||||
}
|
||||
|
||||
public class GitHubDownloadCommand : GitHubBaseCommand
|
||||
{
|
||||
public bool Pre { get; private set; }
|
||||
|
||||
public GitHubDownloadCommand()
|
||||
: base("github", "Download latest release from GitHub repository.")
|
||||
{
|
||||
AddOption<bool>((v) => Pre = v, "--pre")
|
||||
.SetDescription("Get latest pre-release instead of stable.");
|
||||
}
|
||||
}
|
||||
|
||||
public class GitHubUploadCommand : GitHubBaseCommand
|
||||
{
|
||||
public bool Publish { get; private set; }
|
||||
|
||||
public string ReleaseName { get; private set; }
|
||||
|
||||
public GitHubUploadCommand()
|
||||
: base("github", "Upload releases to a GitHub repository.")
|
||||
{
|
||||
AddOption<bool>((v) => Publish = v, "--publish")
|
||||
.SetDescription("Publish release instead of creating draft.");
|
||||
|
||||
AddOption<string>((v) => ReleaseName = v, "--releaseName")
|
||||
.SetDescription("A custom name for created release.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
|
||||
ReleaseDirectoryOption.SetRequired();
|
||||
ReleaseDirectoryOption.MustNotBeEmpty();
|
||||
}
|
||||
}
|
||||
15
src/Squirrel.Csq/Commands/HttpCommands.cs
Normal file
15
src/Squirrel.Csq/Commands/HttpCommands.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Squirrel.Csq.Commands;
|
||||
|
||||
public class HttpDownloadCommand : BaseCommand
|
||||
{
|
||||
public string Url { get; private set; }
|
||||
|
||||
public HttpDownloadCommand()
|
||||
: base("http", "Download latest release from a HTTP source.")
|
||||
{
|
||||
AddOption<Uri>((v) => Url = v.ToAbsoluteOrNull(), "--url")
|
||||
.SetDescription("Url to download remote releases from.")
|
||||
.MustBeValidHttpUri()
|
||||
.SetRequired();
|
||||
}
|
||||
}
|
||||
161
src/Squirrel.Csq/Commands/OsxCommands.cs
Normal file
161
src/Squirrel.Csq/Commands/OsxCommands.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
namespace Squirrel.Csq.Commands;
|
||||
|
||||
public class BundleOsxCommand : BaseCommand
|
||||
{
|
||||
public string PackId { get; private set; }
|
||||
|
||||
public string PackVersion { get; private set; }
|
||||
|
||||
public string PackDirectory { get; private set; }
|
||||
|
||||
public string PackAuthors { get; private set; }
|
||||
|
||||
public string PackTitle { get; private set; }
|
||||
|
||||
public string EntryExecutableName { get; private set; }
|
||||
|
||||
public string Icon { get; private set; }
|
||||
|
||||
public string BundleId { get; private set; }
|
||||
|
||||
public BundleOsxCommand()
|
||||
: base("bundle", "Create's an OSX .app bundle from a folder containing application files.")
|
||||
{
|
||||
AddOption<string>((v) => PackId = v, "--packId", "-u")
|
||||
.SetDescription("Unique Squirrel Id for application bundle.")
|
||||
.SetArgumentHelpName("ID")
|
||||
.SetRequired()
|
||||
.RequiresValidNuGetId();
|
||||
|
||||
// TODO add parser straight to SemanticVersion?
|
||||
AddOption<string>((v) => PackVersion = v, "--packVersion", "-v")
|
||||
.SetDescription("Current version for application bundle.")
|
||||
.SetArgumentHelpName("VERSION")
|
||||
.SetRequired()
|
||||
.RequiresSemverCompliant();
|
||||
|
||||
AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
|
||||
.SetDescription("Directory containing application files for release.")
|
||||
.SetArgumentHelpName("DIR")
|
||||
.SetRequired()
|
||||
.MustNotBeEmpty();
|
||||
|
||||
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
|
||||
.SetDescription("Company name or comma-delimited list of authors.")
|
||||
.SetArgumentHelpName("AUTHORS");
|
||||
|
||||
AddOption<string>((v) => PackTitle = v, "--packTitle")
|
||||
.SetDescription("Display/friendly name for application.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
|
||||
AddOption<string>((v) => EntryExecutableName = v, "-e", "--mainExe")
|
||||
.SetDescription("The file name of the main/entry executable.")
|
||||
.SetArgumentHelpName("NAME")
|
||||
.SetRequired();
|
||||
|
||||
AddOption<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
|
||||
.SetDescription("Path to the .icns file for this bundle.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly()
|
||||
.SetRequired()
|
||||
.RequiresExtension(".icns");
|
||||
|
||||
AddOption<string>((v) => BundleId = v, "--bundleId")
|
||||
.SetDescription("Optional Apple bundle Id.")
|
||||
.SetArgumentHelpName("ID");
|
||||
}
|
||||
}
|
||||
|
||||
public class ReleasifyOsxCommand : BaseCommand
|
||||
{
|
||||
public string BundleDirectory { get; private set; }
|
||||
|
||||
public bool IncludePdb { get; private set; }
|
||||
|
||||
public string ReleaseNotes { get; private set; }
|
||||
|
||||
public bool NoDelta { get; private set; }
|
||||
|
||||
public bool NoPackage { get; private set; }
|
||||
|
||||
public string PackageWelcome { get; private set; }
|
||||
|
||||
public string PackageReadme { get; private set; }
|
||||
|
||||
public string PackageLicense { get; private set; }
|
||||
|
||||
public string PackageConclusion { get; private set; }
|
||||
|
||||
public string SigningAppIdentity { get; private set; }
|
||||
|
||||
public string SigningInstallIdentity { get; private set; }
|
||||
|
||||
public string SigningEntitlements { get; private set; }
|
||||
|
||||
public string NotaryProfile { get; private set; }
|
||||
|
||||
public ReleasifyOsxCommand()
|
||||
: base("releasify", "Converts an application bundle into a Squirrel release and installer.")
|
||||
{
|
||||
AddOption<DirectoryInfo>((v) => BundleDirectory = v.ToFullNameOrNull(), "-b", "--bundle")
|
||||
.SetDescription("The bundle to convert into a Squirrel release.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.MustNotBeEmpty()
|
||||
.RequiresExtension(".app")
|
||||
.SetRequired();
|
||||
|
||||
AddOption<bool>((v) => IncludePdb = v, "--includePdb")
|
||||
.SetDescription("Add *.pdb files to release package.");
|
||||
|
||||
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
|
||||
.SetDescription("File with markdown-formatted notes for this version.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly();
|
||||
|
||||
AddOption<bool>((v) => NoDelta = v, "--noDelta")
|
||||
.SetDescription("Skip the generation of delta packages.");
|
||||
|
||||
if (SquirrelRuntimeInfo.IsOSX) {
|
||||
AddOption<bool>((v) => NoPackage = v, "--noPkg")
|
||||
.SetDescription("Skip generating a .pkg installer.");
|
||||
|
||||
AddOption<FileInfo>((v) => PackageWelcome = v.ToFullNameOrNull(), "--pkgWelcome")
|
||||
.SetDescription("Set the installer package welcome content.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly();
|
||||
|
||||
AddOption<FileInfo>((v) => PackageReadme = v.ToFullNameOrNull(), "--pkgReadme")
|
||||
.SetDescription("Set the installer package readme content.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly();
|
||||
|
||||
AddOption<FileInfo>((v) => PackageLicense = v.ToFullNameOrNull(), "--pkgLicense")
|
||||
.SetDescription("Set the installer package license content.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly();
|
||||
|
||||
AddOption<FileInfo>((v) => PackageConclusion = v.ToFullNameOrNull(), "--pkgConclusion")
|
||||
.SetDescription("Set the installer package conclusion content.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly();
|
||||
|
||||
AddOption<string>((v) => SigningAppIdentity = v, "--signAppIdentity")
|
||||
.SetDescription("The subject name of the cert to use for app code signing.")
|
||||
.SetArgumentHelpName("SUBJECT");
|
||||
|
||||
AddOption<string>((v) => SigningInstallIdentity = v, "--signInstallIdentity")
|
||||
.SetDescription("The subject name of the cert to use for installation packages.")
|
||||
.SetArgumentHelpName("SUBJECT");
|
||||
|
||||
AddOption<FileInfo>((v) => SigningEntitlements = v.ToFullNameOrNull(), "--signEntitlements")
|
||||
.SetDescription("Path to entitlements file for hardened runtime signing.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly()
|
||||
.RequiresExtension(".entitlements");
|
||||
|
||||
AddOption<string>((v) => NotaryProfile = v, "--notaryProfile")
|
||||
.SetDescription("Name of profile containing Apple credentials stored with notarytool.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/Squirrel.Csq/Commands/S3Commands.cs
Normal file
98
src/Squirrel.Csq/Commands/S3Commands.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
namespace Squirrel.Csq.Commands;
|
||||
|
||||
public class S3BaseCommand : BaseCommand
|
||||
{
|
||||
public string KeyId { get; private set; }
|
||||
|
||||
public string Secret { get; private set; }
|
||||
|
||||
public string Region { get; private set; }
|
||||
|
||||
public string Endpoint { get; private set; }
|
||||
|
||||
public string Bucket { get; private set; }
|
||||
|
||||
public string PathPrefix { get; private set; }
|
||||
|
||||
protected S3BaseCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
AddOption<string>((v) => KeyId = v, "--keyId")
|
||||
.SetDescription("Authentication identifier or access key.")
|
||||
.SetArgumentHelpName("KEYID")
|
||||
.SetRequired();
|
||||
|
||||
AddOption<string>((v) => Secret = v, "--secret")
|
||||
.SetDescription("Authentication secret key.")
|
||||
.SetArgumentHelpName("KEY")
|
||||
.SetRequired();
|
||||
|
||||
var region = AddOption<string>((v) => Region = v, "--region")
|
||||
.SetDescription("AWS service region (eg. us-west-1).")
|
||||
.SetArgumentHelpName("REGION");
|
||||
|
||||
region.Validators.Add(MustBeValidAwsRegion);
|
||||
|
||||
var endpoint = AddOption<Uri>((v) => Endpoint = v.ToAbsoluteOrNull(), "--endpoint")
|
||||
.SetDescription("Custom service url (backblaze, digital ocean, etc).")
|
||||
.SetArgumentHelpName("URL")
|
||||
.MustBeValidHttpUri();
|
||||
|
||||
this.AreMutuallyExclusive(region, endpoint);
|
||||
this.AtLeastOneRequired(region, endpoint);
|
||||
|
||||
AddOption<string>((v) => Bucket = v, "--bucket")
|
||||
.SetDescription("Name of the S3 bucket.")
|
||||
.SetArgumentHelpName("NAME")
|
||||
.SetRequired();
|
||||
|
||||
AddOption<string>((v) => PathPrefix = v, "--pathPrefix")
|
||||
.SetDescription("A sub-folder used for files in the bucket, for creating release channels (eg. 'stable' or 'dev').")
|
||||
.SetArgumentHelpName("PREFIX");
|
||||
}
|
||||
|
||||
private static void MustBeValidAwsRegion(OptionResult result)
|
||||
{
|
||||
for (var i = 0; i < result.Tokens.Count; i++) {
|
||||
var region = result.Tokens[i].Value;
|
||||
if (!string.IsNullOrWhiteSpace(region)) {
|
||||
var r = Amazon.RegionEndpoint.GetBySystemName(result.Tokens[0].Value);
|
||||
if (r is null || r.DisplayName == "Unknown") {
|
||||
result.AddError($"Region '{region}' lookup failed, is this a valid AWS region?");
|
||||
}
|
||||
} else {
|
||||
result.AddError("A region value is required");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class S3DownloadCommand : S3BaseCommand
|
||||
{
|
||||
public S3DownloadCommand()
|
||||
: base("s3", "Download latest release from an S3 bucket.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class S3UploadCommand : S3BaseCommand
|
||||
{
|
||||
public bool Overwrite { get; private set; }
|
||||
|
||||
public int KeepMaxReleases { get; private set; }
|
||||
|
||||
public S3UploadCommand()
|
||||
: base("s3", "Upload releases to a S3 bucket.")
|
||||
{
|
||||
AddOption<bool>((v) => Overwrite = v, "--overwrite")
|
||||
.SetDescription("Replace remote files if local files have changed.");
|
||||
|
||||
AddOption<int>((v) => KeepMaxReleases = v, "--keepMaxReleases")
|
||||
.SetDescription("Apply a retention policy which keeps only the specified number of old versions in remote source.")
|
||||
.SetArgumentHelpName("NUMBER");
|
||||
|
||||
ReleaseDirectoryOption.SetRequired();
|
||||
ReleaseDirectoryOption.MustNotBeEmpty();
|
||||
}
|
||||
}
|
||||
308
src/Squirrel.Csq/Commands/SystemCommandLineExtensions.cs
Normal file
308
src/Squirrel.Csq/Commands/SystemCommandLineExtensions.cs
Normal file
@@ -0,0 +1,308 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using NuGet.Versioning;
|
||||
|
||||
namespace Squirrel.Csq.Commands;
|
||||
|
||||
internal static class SystemCommandLineExtensions
|
||||
{
|
||||
public static string ToFullNameOrNull(this FileSystemInfo fsi)
|
||||
{
|
||||
return fsi?.FullName;
|
||||
}
|
||||
|
||||
public static string ToAbsoluteOrNull(this Uri uri)
|
||||
{
|
||||
if (uri?.IsAbsoluteUri == true) return uri.AbsoluteUri;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static CliOption<T> SetDescription<T>(this CliOption<T> option, string description)
|
||||
{
|
||||
option.Description = description;
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<T> SetHidden<T>(this CliOption<T> option, bool isHidden = true)
|
||||
{
|
||||
option.Hidden = isHidden;
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<T> SetRequired<T>(this CliOption<T> option, bool isRequired = true)
|
||||
{
|
||||
option.Required = isRequired;
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<T> SetDefault<T>(this CliOption<T> option, T defaultValue)
|
||||
{
|
||||
option.SetDefault(defaultValue);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<T> SetArgumentHelpName<T>(this CliOption<T> option, string argumentHelpName)
|
||||
{
|
||||
option.HelpName = argumentHelpName;
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<int> MustBeBetween(this CliOption<int> option, int minimum, int maximum)
|
||||
{
|
||||
option.Validators.Add(x => Validate.MustBeBetween(x, minimum, maximum));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<Uri> MustBeValidHttpUri(this CliOption<Uri> option)
|
||||
{
|
||||
option.RequiresScheme(Uri.UriSchemeHttp, Uri.UriSchemeHttps).RequiresAbsolute();
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<FileInfo> RequiresExtension(this CliOption<FileInfo> option, string extension)
|
||||
{
|
||||
option.Validators.Add(x => Validate.RequiresExtension(x, extension));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<DirectoryInfo> RequiresExtension(this CliOption<DirectoryInfo> option, string extension)
|
||||
{
|
||||
option.Validators.Add(x => Validate.RequiresExtension(x, extension));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliCommand AreMutuallyExclusive(this CliCommand command, params CliOption[] options)
|
||||
{
|
||||
command.Validators.Add(x => Validate.AreMutuallyExclusive(x, options));
|
||||
return command;
|
||||
}
|
||||
|
||||
//public static Command RequiredAllowObsoleteFallback(this Command command, Option option, Option obsoleteOption)
|
||||
//{
|
||||
// command.AddValidator(x => Validate.AtLeastOneRequired(x, new[] { option, obsoleteOption }, true));
|
||||
// return command;
|
||||
//}
|
||||
|
||||
public static CliCommand AtLeastOneRequired(this CliCommand command, params CliOption[] options)
|
||||
{
|
||||
command.Validators.Add(x => Validate.AtLeastOneRequired(x, options, false));
|
||||
return command;
|
||||
}
|
||||
|
||||
public static CliOption<string> MustContain(this CliOption<string> option, string value)
|
||||
{
|
||||
option.Validators.Add(x => Validate.MustContain(x, value));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<Uri> RequiresScheme(this CliOption<Uri> option, params string[] validSchemes)
|
||||
{
|
||||
option.Validators.Add(x => Validate.RequiresScheme(x, validSchemes));
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<Uri> RequiresAbsolute(this CliOption<Uri> option, params string[] validSchemes)
|
||||
{
|
||||
option.Validators.Add(Validate.RequiresAbsolute);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<string> RequiresValidNuGetId(this CliOption<string> option)
|
||||
{
|
||||
option.Validators.Add(Validate.RequiresValidNuGetId);
|
||||
return option;
|
||||
}
|
||||
|
||||
//TODO: Could setup the options to accept type SemanticVersion and apply an appropriate parser for it
|
||||
public static CliOption<string> RequiresSemverCompliant(this CliOption<string> option)
|
||||
{
|
||||
option.Validators.Add(Validate.RequiresSemverCompliant);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<DirectoryInfo> MustNotBeEmpty(this CliOption<DirectoryInfo> option)
|
||||
{
|
||||
option.Validators.Add(Validate.MustNotBeEmpty);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<string> MustBeValidFrameworkString(this CliOption<string> option)
|
||||
{
|
||||
option.Validators.Add(Validate.MustBeValidFrameworkString);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<string> MustBeValidMsiVersion(this CliOption<string> option)
|
||||
{
|
||||
option.Validators.Add(Validate.MustBeValidMsiVersion);
|
||||
return option;
|
||||
}
|
||||
|
||||
public static CliOption<string> MustBeSupportedRid(this CliOption<string> option)
|
||||
{
|
||||
option.Validators.Add(Validate.MustBeSupportedRid);
|
||||
return option;
|
||||
}
|
||||
|
||||
private static class Validate
|
||||
{
|
||||
public static void MustBeBetween(OptionResult result, int minimum, int maximum)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (int.TryParse(result.Tokens[i].Value, out int value)) {
|
||||
if (value < minimum || value > maximum) {
|
||||
result.AddError($"The value for {result.IdentifierToken.Value} must be greater than {minimum} and less than {maximum}");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
result.AddError($"{result.Tokens[i].Value} is not a valid integer for {result.IdentifierToken.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresExtension(OptionResult result, string extension)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (!string.Equals(Path.GetExtension(result.Tokens[i].Value), extension, StringComparison.InvariantCultureIgnoreCase)) {
|
||||
result.AddError($"{result.IdentifierToken.Value} does not have an {extension} extension");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AreMutuallyExclusive(CommandResult result, CliOption[] options)
|
||||
{
|
||||
var specifiedOptions = options
|
||||
.Where(x => result.GetResult(x) is not null)
|
||||
.ToList();
|
||||
if (specifiedOptions.Count > 1) {
|
||||
string optionsString = string.Join(" and ", specifiedOptions.Select(x => $"'{x.Aliases.First()}'"));
|
||||
result.AddError($"Cannot use {optionsString} options together, please choose one.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void AtLeastOneRequired(CommandResult result, CliOption[] options, bool onlyShowFirst = false)
|
||||
{
|
||||
var anySpecifiedOptions = options
|
||||
.Any(x => result.GetResult(x) is not null);
|
||||
if (!anySpecifiedOptions) {
|
||||
if (onlyShowFirst) {
|
||||
result.AddError($"Required argument missing for option: {options.First().Aliases.First()}");
|
||||
} else {
|
||||
string optionsString = string.Join(" and ", options.Select(x => $"'{x.Aliases.First()}'"));
|
||||
result.AddError($"At least one of the following options are required {optionsString}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustContain(OptionResult result, string value)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (result.Tokens[i].Value?.Contains(value) == false) {
|
||||
result.AddError($"{result.IdentifierToken.Value} must contain '{value}'. Current value is '{result.Tokens[i].Value}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresScheme(OptionResult result, string[] validSchemes)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (Uri.TryCreate(result.Tokens[i].Value, UriKind.RelativeOrAbsolute, out Uri uri) &&
|
||||
uri.IsAbsoluteUri &&
|
||||
!validSchemes.Contains(uri.Scheme)) {
|
||||
result.AddError($"{result.IdentifierToken.Value} must contain a Uri with one of the following schems: {string.Join(", ", validSchemes)}. Current value is '{result.Tokens[i].Value}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresAbsolute(OptionResult result)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (!Uri.TryCreate(result.Tokens[i].Value, UriKind.Absolute, out Uri _)) {
|
||||
result.AddError($"{result.IdentifierToken.Value} must contain an absolute Uri. Current value is '{result.Tokens[i].Value}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresValidNuGetId(OptionResult result)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (!NugetUtil.IsValidNuGetId(result.Tokens[i].Value)) {
|
||||
result.AddError($"{result.IdentifierToken.Value} is an invalid NuGet package id. It must contain only alphanumeric characters, underscores, dashes, and dots.. Current value is '{result.Tokens[i].Value}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequiresSemverCompliant(OptionResult result)
|
||||
{
|
||||
string specifiedAlias = result.IdentifierToken.Value;
|
||||
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
string version = result.Tokens[i].Value;
|
||||
//TODO: This is duplicating NugetUtil.ThrowIfVersionNotSemverCompliant
|
||||
if (SemanticVersion.TryParse(version, out var parsed)) {
|
||||
if (parsed < new SemanticVersion(0, 0, 1)) {
|
||||
result.AddError($"{result.IdentifierToken.Value} contains an invalid package version '{version}', it must be >= 0.0.1.");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
result.AddError($"{result.IdentifierToken.Value} contains an invalid package version '{version}', it must be a 3-part SemVer2 compliant version string.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustNotBeEmpty(OptionResult result)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
var token = result.Tokens[i];
|
||||
|
||||
if (!Directory.Exists(token.Value) ||
|
||||
!Directory.EnumerateFileSystemEntries(token.Value).Any()) {
|
||||
result.AddError($"{result.IdentifierToken.Value} must be a non-empty directory, but the specified directory '{token.Value}' was empty.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustBeValidFrameworkString(OptionResult result)
|
||||
{
|
||||
for (var i = 0; i < result.Tokens.Count; i++) {
|
||||
var framework = result.Tokens[i].Value;
|
||||
try {
|
||||
Runtimes.ParseDependencyString(framework);
|
||||
} catch (Exception e) {
|
||||
result.AddError(e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustBeValidMsiVersion(OptionResult result)
|
||||
{
|
||||
for (var i = 0; i < result.Tokens.Count; i++) {
|
||||
var version = result.Tokens[i].Value;
|
||||
if (Version.TryParse(version, out var parsed)) {
|
||||
if (parsed.Major > 255 || parsed.Minor > 255 || parsed.Build > 65535 || parsed.Revision > 0) {
|
||||
result.AddError($"MSI ProductVersion out of bounds '{version}'. Valid range is [0-255].[0-255].[0-65535].[0]");
|
||||
}
|
||||
} else {
|
||||
result.AddError("Version string is invalid / could not be parsed.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void MustBeSupportedRid(OptionResult result)
|
||||
{
|
||||
for (int i = 0; i < result.Tokens.Count; i++) {
|
||||
if (!Regex.IsMatch(result.Tokens[i].Value, @"^(?<os>osx|win)\.?(?<ver>[\d\.]+)?(?:-(?<arch>(?:x|arm)\d{2}))$"))
|
||||
result.AddError($"Invalid or unsupported runtime '{result.IdentifierToken.Value}'. Valid example: win-x64, osx-arm64.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
192
src/Squirrel.Csq/Commands/WindowsCommands.cs
Normal file
192
src/Squirrel.Csq/Commands/WindowsCommands.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
|
||||
namespace Squirrel.Csq.Commands;
|
||||
|
||||
public class SigningCommand : BaseCommand
|
||||
{
|
||||
public string SignParameters { get; private set; }
|
||||
|
||||
public bool SignSkipDll { get; private set; }
|
||||
|
||||
public int SignParallel { get; private set; }
|
||||
|
||||
public string SignTemplate { get; private set; }
|
||||
|
||||
protected SigningCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
var signTemplate = AddOption<string>((v) => SignTemplate = v, "--signTemplate")
|
||||
.SetDescription("Use a custom signing command. {{file}} will be replaced by the path to sign.")
|
||||
.SetArgumentHelpName("COMMAND")
|
||||
.MustContain("{{file}}");
|
||||
|
||||
AddOption<bool>((v) => SignSkipDll = v, "--signSkipDll")
|
||||
.SetDescription("Only signs EXE files, and skips signing DLL files.");
|
||||
|
||||
if (SquirrelRuntimeInfo.IsWindows) {
|
||||
var signParams = AddOption<string>((v) => SignParameters = v, "--signParams", "-n")
|
||||
.SetDescription("Sign files via signtool.exe using these parameters.")
|
||||
.SetArgumentHelpName("PARAMS");
|
||||
|
||||
this.AreMutuallyExclusive(signTemplate, signParams);
|
||||
|
||||
AddOption<int>((v) => SignParallel = v, "--signParallel")
|
||||
.SetDescription("The number of files to sign in each call to signtool.exe.")
|
||||
.SetArgumentHelpName("NUM")
|
||||
.MustBeBetween(1, 1000)
|
||||
.SetDefault(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ReleasifyWindowsCommand : SigningCommand
|
||||
{
|
||||
public string Package { get; set; }
|
||||
|
||||
public string BaseUrl { get; private set; }
|
||||
|
||||
public string DebugSetupExe { get; private set; }
|
||||
|
||||
public bool NoDelta { get; private set; }
|
||||
|
||||
public string Runtimes { get; private set; }
|
||||
|
||||
public string SplashImage { get; private set; }
|
||||
|
||||
public string Icon { get; private set; }
|
||||
|
||||
public string[] SquirrelAwareExecutableNames { get; private set; }
|
||||
|
||||
public string AppIcon { get; private set; }
|
||||
|
||||
public bool BuildMsi { get; private set; }
|
||||
|
||||
public string MsiVersion { get; private set; }
|
||||
|
||||
public ReleasifyWindowsCommand()
|
||||
: this("releasify", "Take an existing nuget package and convert it into a Squirrel release.")
|
||||
{
|
||||
AddOption<FileInfo>((v) => Package = v.ToFullNameOrNull(), "-p", "--package")
|
||||
.SetDescription("Path to a '.nupkg' package to releasify.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.SetRequired()
|
||||
.AcceptExistingOnly()
|
||||
.RequiresExtension(".nupkg");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This constructor is used by the pack command, which requires all the same properties but
|
||||
/// does not allow the user to provide the Package (it is created/populated by Squirrel).
|
||||
/// </summary>
|
||||
protected ReleasifyWindowsCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
AddOption<Uri>((v) => BaseUrl = v.ToAbsoluteOrNull(), "-b", "--baseUrl")
|
||||
.SetDescription("Provides a base URL to prefix the RELEASES file packages with.")
|
||||
.SetHidden()
|
||||
.MustBeValidHttpUri();
|
||||
|
||||
AddOption<FileInfo>((v) => DebugSetupExe = v.ToFullNameOrNull(), "--debugSetupExe")
|
||||
.SetDescription("Uses the Setup.exe at this {PATH} to create the bundle, and then replaces it with the bundle. " +
|
||||
"Used for locally debugging Setup.exe with a real bundle attached.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.SetHidden()
|
||||
.AcceptExistingOnly()
|
||||
.RequiresExtension(".exe");
|
||||
|
||||
AddOption<bool>((v) => NoDelta = v, "--noDelta")
|
||||
.SetDescription("Skip the generation of delta packages.");
|
||||
|
||||
AddOption<string>((v) => Runtimes = v, "-f", "--framework")
|
||||
.SetDescription("List of required runtimes to install during setup. example: 'net6,vcredist143'.")
|
||||
.SetArgumentHelpName("RUNTIMES")
|
||||
.MustBeValidFrameworkString();
|
||||
|
||||
AddOption<FileInfo>((v) => SplashImage = v.ToFullNameOrNull(), "-s", "--splashImage")
|
||||
.SetDescription("Path to image displayed during installation.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly();
|
||||
|
||||
AddOption<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
|
||||
.SetDescription("Path to .ico for Setup.exe and Update.exe.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly()
|
||||
.RequiresExtension(".ico");
|
||||
|
||||
AddOption<string[]>((v) => SquirrelAwareExecutableNames = v ?? new string[0], "-e", "--mainExe")
|
||||
.SetDescription("Name of one or more SquirrelAware executables.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
|
||||
AddOption<FileInfo>((v) => AppIcon = v.ToFullNameOrNull(), "--appIcon")
|
||||
.SetDescription("Path to .ico for 'Apps and Features' list.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly()
|
||||
.RequiresExtension(".ico");
|
||||
|
||||
if (SquirrelRuntimeInfo.IsWindows) {
|
||||
AddOption<bool>((v) => BuildMsi = v, "--msi")
|
||||
.SetDescription("Compile a .msi machine-wide deployment tool.")
|
||||
.SetArgumentHelpName("BITNESS");
|
||||
|
||||
AddOption<string>((v) => MsiVersion = v, "--msiVersion")
|
||||
.SetDescription("Override the product version for the generated msi.")
|
||||
.SetArgumentHelpName("VERSION")
|
||||
.MustBeValidMsiVersion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PackWindowsCommand : ReleasifyWindowsCommand, INugetPackCommand
|
||||
{
|
||||
public string PackId { get; private set; }
|
||||
|
||||
public string PackVersion { get; private set; }
|
||||
|
||||
public string PackDirectory { get; private set; }
|
||||
|
||||
public string PackAuthors { get; private set; }
|
||||
|
||||
public string PackTitle { get; private set; }
|
||||
|
||||
public bool IncludePdb { get; private set; }
|
||||
|
||||
public string ReleaseNotes { get; private set; }
|
||||
|
||||
public PackWindowsCommand()
|
||||
: base("pack", "Creates a Squirrel release from a folder containing application files.")
|
||||
{
|
||||
AddOption<string>((v) => PackId = v, "--packId", "-u")
|
||||
.SetDescription("Unique Id for application bundle.")
|
||||
.SetArgumentHelpName("ID")
|
||||
.SetRequired()
|
||||
.RequiresValidNuGetId();
|
||||
|
||||
// TODO add parser straight to SemanticVersion
|
||||
AddOption<string>((v) => PackVersion = v, "--packVersion", "-v")
|
||||
.SetDescription("Current version for application bundle.")
|
||||
.SetArgumentHelpName("VERSION")
|
||||
.SetRequired()
|
||||
.RequiresSemverCompliant();
|
||||
|
||||
AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
|
||||
.SetDescription("Directory containing application files for release.")
|
||||
.SetArgumentHelpName("DIR")
|
||||
.SetRequired()
|
||||
.MustNotBeEmpty();
|
||||
|
||||
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
|
||||
.SetDescription("Company name or comma-delimited list of authors.")
|
||||
.SetArgumentHelpName("AUTHORS");
|
||||
|
||||
AddOption<string>((v) => PackTitle = v, "--packTitle")
|
||||
.SetDescription("Display/friendly name for application.")
|
||||
.SetArgumentHelpName("NAME");
|
||||
|
||||
AddOption<bool>((v) => IncludePdb = v, "--includePdb")
|
||||
.SetDescription("Add *.pdb files to release package");
|
||||
|
||||
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
|
||||
.SetDescription("File with markdown-formatted notes for this version.")
|
||||
.SetArgumentHelpName("PATH")
|
||||
.AcceptExistingOnly();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
#if false
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Builder;
|
||||
@@ -12,8 +13,9 @@ using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Build.Construction;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Squirrel.CommandLine;
|
||||
using LogLevel = Squirrel.SimpleSplat.LogLevel;
|
||||
|
||||
namespace Squirrel.Tool
|
||||
{
|
||||
@@ -23,18 +25,26 @@ namespace Squirrel.Tool
|
||||
|
||||
private static ConsoleLogger _logger;
|
||||
|
||||
private static Option<string> CsqVersion { get; }
|
||||
= new Option<string>("--csq-version");
|
||||
private static Option<FileSystemInfo> CsqSolutionPath { get; }
|
||||
= new Option<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly();
|
||||
private static Option<bool> Verbose { get; }
|
||||
= new Option<bool>("--verbose");
|
||||
private static CliOption<string> CsqVersion { get; }
|
||||
= new CliOption<string>("--csq-version");
|
||||
private static CliOption<FileSystemInfo> CsqSolutionPath { get; }
|
||||
= new CliOption<FileSystemInfo>(new[] { "--csq-sln", "--csq-solution" }).ExistingOnly();
|
||||
private static CliOption<bool> Verbose { get; }
|
||||
= new CliOption<bool>("--verbose");
|
||||
|
||||
static Task<int> Main(string[] inargs)
|
||||
static Task<int> Main(string[] args)
|
||||
{
|
||||
|
||||
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
|
||||
builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
|
||||
builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
|
||||
builder.Configuration.AddCommandLine(args);
|
||||
_logger = ConsoleLogger.RegisterLogger();
|
||||
|
||||
RootCommand rootCommand = new RootCommand() {
|
||||
System.CommandLine.Hosting.HostingExtensions.
|
||||
|
||||
CliRootCommand rootCommand = new CliRootCommand() {
|
||||
CsqVersion,
|
||||
CsqSolutionPath,
|
||||
Verbose
|
||||
7
src/Squirrel.Csq/GlobalUsings.cs
Normal file
7
src/Squirrel.Csq/GlobalUsings.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.CommandLine;
|
||||
global using System.CommandLine.Parsing;
|
||||
global using System.IO;
|
||||
global using System.Linq;
|
||||
global using System.Threading.Tasks;
|
||||
80
src/Squirrel.Csq/Program.cs
Normal file
80
src/Squirrel.Csq/Program.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Serilog.Events;
|
||||
using Serilog;
|
||||
using CliFx;
|
||||
|
||||
namespace Squirrel.Csq;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
var builder = Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings {
|
||||
Args = args,
|
||||
ApplicationName = "My App",
|
||||
EnvironmentName = "Production",
|
||||
ContentRootPath = Environment.CurrentDirectory,
|
||||
Configuration = new ConfigurationManager(),
|
||||
});
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
host.Services.
|
||||
|
||||
|
||||
return await new CliApplicationBuilder()
|
||||
.AddCommandsFromThisAssembly()
|
||||
.Build()
|
||||
.RunAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static Parser CreateParser()
|
||||
{
|
||||
//var rootCommand = new RootCommand("Bicep registry module tool")
|
||||
// .AddSubcommand(new ValidateCommand())
|
||||
// .AddSubcommand(new GenerateCommand());
|
||||
|
||||
var parser = new CliConfiguration(null)
|
||||
.UseHost(Host.CreateDefaultBuilder, ConfigureHost)
|
||||
.UseDefaults()
|
||||
.UseVerboseOption()
|
||||
.Build();
|
||||
|
||||
// Have to use parser.Invoke instead of rootCommand.Invoke due to the
|
||||
// System.CommandLine bug: https://github.com/dotnet/command-line-api/issues/1691.
|
||||
rootCommand.Handler = CommandHandler.Create(() => parser.Invoke("-h"));
|
||||
|
||||
return parser;
|
||||
}
|
||||
|
||||
private static IHostBuilder CreateBuilder(string[] args)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private static void ConfigureHost(IHostBuilder builder)
|
||||
{
|
||||
builder.UseSerilog((context, logging) => logging
|
||||
.MinimumLevel.Is(GetMinimumLogEventLevel(context))
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
||||
.WriteTo.Console())
|
||||
.UseCommandHandlers();
|
||||
}
|
||||
|
||||
private static LogEventLevel GetMinimumLogEventLevel(HostBuilderContext context)
|
||||
{
|
||||
var verboseSpecified =
|
||||
context.Properties.TryGetValue(typeof(InvocationContext), out var value) &&
|
||||
value is InvocationContext invocationContext &&
|
||||
invocationContext.ParseResult.FindResultFor(GlobalOptions.Verbose) is not null;
|
||||
|
||||
return verboseSpecified ? LogEventLevel.Debug : LogEventLevel.Fatal;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<IsPackable>true</IsPackable>
|
||||
<AssemblyName>csq</AssemblyName>
|
||||
<PackageId>csq</PackageId>
|
||||
@@ -13,21 +13,20 @@
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<Description>A .NET Core Tool that uses the Squirrel framework to create installers and update packages for dotnet applications.</Description>
|
||||
<PackageIcon>Clowd_200.png</PackageIcon>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\docs\artwork\Clowd_200.png" Pack="true" PackagePath="\" />
|
||||
<Compile Include="..\Squirrel.CommandLine\ConsoleLogger.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build" Version="17.3.2" />
|
||||
<PackageReference Include="NuGet.Protocol" Version="6.7.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.23407.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
155
src/Squirrel.Deployment/GitHubRepository.cs
Normal file
155
src/Squirrel.Deployment/GitHubRepository.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Octokit;
|
||||
using Squirrel.Extensions;
|
||||
using Squirrel.Sources;
|
||||
|
||||
namespace Squirrel.CommandLine.Sync;
|
||||
|
||||
public class GitHubRepository
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
|
||||
public GitHubRepository(ILogger logger)
|
||||
{
|
||||
_log = logger;
|
||||
}
|
||||
|
||||
public async Task DownloadRecentPackages(DirectoryInfo releaseDirectoryInfo, string repoUrl, string token, bool asPrerelease)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(token))
|
||||
_log.Warn("No GitHub access token provided. Unauthenticated requests will be limited to 60 per hour.");
|
||||
|
||||
_log.Info("Fetching RELEASES...");
|
||||
var source = new GithubSource(repoUrl, token, asPrerelease);
|
||||
var latestReleaseEntries = await source.GetReleaseFeed();
|
||||
|
||||
if (latestReleaseEntries == null || latestReleaseEntries.Length == 0) {
|
||||
_log.Warn("No github release or assets found.");
|
||||
return;
|
||||
}
|
||||
|
||||
_log.Info($"Found {latestReleaseEntries.Length} assets in RELEASES file for GitHub version {source.Release.Name}.");
|
||||
|
||||
var releasesToDownload = latestReleaseEntries
|
||||
.Where(x => !x.IsDelta)
|
||||
.OrderByDescending(x => x.Version)
|
||||
.Take(1)
|
||||
.Select(x => new {
|
||||
Obj = x,
|
||||
LocalPath = Path.Combine(releaseDirectoryInfo.FullName, x.Filename),
|
||||
Filename = x.Filename,
|
||||
});
|
||||
|
||||
foreach (var entry in releasesToDownload) {
|
||||
if (File.Exists(entry.LocalPath)) {
|
||||
_log.Warn($"File '{entry.Filename}' exists on disk, skipping download.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_log.Info($"Downloading {entry.Filename}...");
|
||||
await source.DownloadReleaseEntry(entry.Obj, entry.LocalPath, (p) => { });
|
||||
}
|
||||
|
||||
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
||||
_log.Info("Done.");
|
||||
}
|
||||
|
||||
public static async Task UploadMissingPackages(GitHubUploadCommand options)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(options.Token))
|
||||
throw new InvalidOperationException("Must provide access token to create a GitHub release.");
|
||||
|
||||
var releaseDirectoryInfo = options.GetReleaseDirectory();
|
||||
|
||||
var repoUri = new Uri(options.RepoUrl);
|
||||
var repoParts = repoUri.AbsolutePath.Trim('/').Split('/');
|
||||
if (repoParts.Length != 2)
|
||||
throw new Exception($"Invalid GitHub URL, '{repoUri.AbsolutePath}' should be in the format 'owner/repo'");
|
||||
|
||||
var repoOwner = repoParts[0];
|
||||
var repoName = repoParts[1];
|
||||
|
||||
var client = new GitHubClient(new ProductHeaderValue("Clowd.Squirrel")) {
|
||||
Credentials = new Credentials(options.Token)
|
||||
};
|
||||
|
||||
var releasesPath = Path.Combine(releaseDirectoryInfo.FullName, "RELEASES");
|
||||
if (!File.Exists(releasesPath))
|
||||
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
||||
|
||||
var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath)).ToArray();
|
||||
if (releases.Length == 0)
|
||||
throw new Exception("There are no nupkg's in the releases directory to upload");
|
||||
|
||||
var ver = Enumerable.MaxBy(releases, x => x.Version);
|
||||
if (ver == null)
|
||||
throw new Exception("There are no nupkg's in the releases directory to upload");
|
||||
var semVer = ver.Version;
|
||||
|
||||
_log.Info($"Preparing to upload latest local release to GitHub");
|
||||
|
||||
var newReleaseReq = new NewRelease(semVer.ToString()) {
|
||||
Body = ver.GetReleaseNotes(releaseDirectoryInfo.FullName, ReleaseNotesFormat.Markdown),
|
||||
Draft = true,
|
||||
Prerelease = semVer.HasMetadata || semVer.IsPrerelease,
|
||||
Name = string.IsNullOrWhiteSpace(options.ReleaseName)
|
||||
? semVer.ToString()
|
||||
: options.ReleaseName,
|
||||
};
|
||||
|
||||
_log.Info($"Creating draft release titled '{semVer.ToString()}'");
|
||||
|
||||
var existingReleases = await client.Repository.Release.GetAll(repoOwner, repoName);
|
||||
if (existingReleases.Any(r => r.TagName == semVer.ToString())) {
|
||||
throw new Exception($"There is already an existing release tagged '{semVer}'. Please delete this release or choose a new version number.");
|
||||
}
|
||||
|
||||
var release = await client.Repository.Release.Create(repoOwner, repoName, newReleaseReq);
|
||||
|
||||
// locate files to upload
|
||||
var files = releaseDirectoryInfo.GetFiles("*", SearchOption.TopDirectoryOnly);
|
||||
var msiFile = files.SingleOrDefault(f => f.FullName.EndsWith(".msi", StringComparison.InvariantCultureIgnoreCase));
|
||||
var setupFile = files.Where(f => f.FullName.EndsWith("Setup.exe", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ContextualSingle("release directory", "Setup.exe file");
|
||||
|
||||
var releasesToUpload = releases.Where(x => x.Version == semVer).ToArray();
|
||||
MemoryStream releasesFileToUpload = new MemoryStream();
|
||||
ReleaseEntry.WriteReleaseFile(releasesToUpload, releasesFileToUpload);
|
||||
var releasesBytes = releasesFileToUpload.ToArray();
|
||||
|
||||
// upload nupkg's
|
||||
foreach (var r in releasesToUpload) {
|
||||
var path = Path.Combine(releaseDirectoryInfo.FullName, r.Filename);
|
||||
await UploadFileAsAsset(client, release, path);
|
||||
}
|
||||
|
||||
// other files
|
||||
await UploadFileAsAsset(client, release, setupFile.FullName);
|
||||
if (msiFile != null) await UploadFileAsAsset(client, release, msiFile.FullName);
|
||||
|
||||
// RELEASES
|
||||
_log.Info($"Uploading RELEASES");
|
||||
var data = new ReleaseAssetUpload("RELEASES", "application/octet-stream", new MemoryStream(releasesBytes), TimeSpan.FromMinutes(1));
|
||||
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
||||
|
||||
_log.Info($"Done creating draft GitHub release.");
|
||||
|
||||
// convert draft to full release
|
||||
if (options.Publish) {
|
||||
_log.Info("Converting draft to full published release.");
|
||||
var upd = release.ToUpdate();
|
||||
upd.Draft = false;
|
||||
release = await client.Repository.Release.Edit(repoOwner, repoName, release.Id, upd);
|
||||
}
|
||||
|
||||
_log.Info("Release URL: " + release.HtmlUrl);
|
||||
}
|
||||
|
||||
private static async Task UploadFileAsAsset(GitHubClient client, Release release, string filePath)
|
||||
{
|
||||
_log.Info($"Uploading asset '{Path.GetFileName(filePath)}'");
|
||||
using var stream = File.OpenRead(filePath);
|
||||
var data = new ReleaseAssetUpload(Path.GetFileName(filePath), "application/octet-stream", stream, TimeSpan.FromMinutes(30));
|
||||
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
26
src/Squirrel.Deployment/Squirrel.Deployment.csproj
Normal file
26
src/Squirrel.Deployment/Squirrel.Deployment.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AWSSDK.S3" Version="3.7.205.22" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Octokit" Version="9.0.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageReference Include="System.IO" Version="4.3.0" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="System.Security.Cryptography.Algorithms" Version="4.3.1" />
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageReference Include="NuGet.Commands" Version="6.7.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<PackageReference Include="PeNet" Version="4.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
src/Squirrel.Packaging.OSX/Squirrel.Packaging.OSX.csproj
Normal file
8
src/Squirrel.Packaging.OSX/Squirrel.Packaging.OSX.csproj
Normal file
@@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,423 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
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.SystemOs.GetOsLongName()}' 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) {
|
||||
PlatformUtil.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.DeleteFileOrDirectoryHard(latestDir, renameFirst: true, throwOnFailure: false);
|
||||
Utility.Retry(() => Directory.Move(latestVer.DirectoryPath, latestDir));
|
||||
|
||||
this.Log().Info("Current app is now: " + 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 PlatformUtil.StartProcessNonBlocking(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 = CurrentVersionDir != null ? Utility.FullPathEquals(d, CurrentVersionDir) : false;
|
||||
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()
|
||||
{
|
||||
List<string> directories = new List<string>() { CurrentVersionDir };
|
||||
if (Directory.Exists(RootAppDir))
|
||||
directories.AddRange(Directory.GetDirectories(RootAppDir, "app-*", SearchOption.TopDirectoryOnly));
|
||||
|
||||
if (Directory.Exists(VersionStagingDir))
|
||||
directories.AddRange(Directory.GetDirectories(VersionStagingDir, "app-*", SearchOption.TopDirectoryOnly));
|
||||
|
||||
return directories
|
||||
.Where(Directory.Exists)
|
||||
.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>
|
||||
[SupportedOSPlatform("windows")]
|
||||
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; }
|
||||
|
||||
/// <summary> True if Update.exe is currently performing first app install. </summary>
|
||||
public bool IsInstalling { 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() : this(SquirrelRuntimeInfo.EntryExePath)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal use only. Creates a AppDescWindows from the following rootAppDir and
|
||||
/// does not perform any path auto-detection.
|
||||
/// </summary>
|
||||
internal AppDescWindows(string rootAppDir, string appId, bool isInstalling = false)
|
||||
{
|
||||
AppId = appId;
|
||||
RootAppDir = rootAppDir;
|
||||
var updateExe = Path.Combine(rootAppDir, "Update.exe");
|
||||
UpdateExePath = updateExe;
|
||||
IsUpdateExe = Utility.FullPathEquals(updateExe, SquirrelRuntimeInfo.EntryExePath);
|
||||
CurrentlyInstalledVersion = GetLatestVersion()?.Version;
|
||||
IsInstalling = isInstalling;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal use only. Auto detect app details from the specified EXE path.
|
||||
/// </summary>
|
||||
internal AppDescWindows(string ourExePath)
|
||||
{
|
||||
if (!SquirrelRuntimeInfo.IsWindows)
|
||||
throw new NotSupportedException("Cannot instantiate AppDescWindows on a non-Windows system.");
|
||||
|
||||
ourExePath = Path.GetFullPath(ourExePath);
|
||||
var myDir = Path.GetDirectoryName(ourExePath);
|
||||
|
||||
// Am I update.exe at the application root?
|
||||
if (ourExePath != null &&
|
||||
Path.GetFileName(ourExePath).Equals("update.exe", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
ourExePath.IndexOf("app-", StringComparison.InvariantCultureIgnoreCase) == -1 &&
|
||||
ourExePath.IndexOf("SquirrelClowdTemp", StringComparison.InvariantCultureIgnoreCase) == -1) {
|
||||
UpdateExePath = ourExePath;
|
||||
RootAppDir = myDir;
|
||||
var ver = GetLatestVersion();
|
||||
if (ver != null) {
|
||||
AppId = ver.Manifest?.Id ?? Path.GetFileName(myDir);
|
||||
CurrentlyInstalledVersion = ver.Version;
|
||||
IsUpdateExe = true;
|
||||
} else {
|
||||
UpdateExePath = null;
|
||||
RootAppDir = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Am I running from within an app-* or current dir?
|
||||
// 'info' will be null in any portable / non-installed app.
|
||||
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) {
|
||||
RootAppDir = Path.GetDirectoryName(updateLocation);
|
||||
UpdateExePath = updateLocation;
|
||||
AppId = info.Manifest?.Id ?? Path.GetFileName(Path.GetDirectoryName(updateLocation));
|
||||
CurrentlyInstalledVersion = info.Version;
|
||||
IsUpdateExe = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
[SupportedOSPlatform("osx")]
|
||||
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 SemanticVersion CurrentlyInstalledVersion { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CurrentVersionDir => RootAppDir;
|
||||
|
||||
/// <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 + 4);
|
||||
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,10 +1,11 @@
|
||||
using System;
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Squirrel
|
||||
namespace Squirrel.Compression
|
||||
{
|
||||
internal sealed class BZip2Stream : Stream
|
||||
public sealed class BZip2Stream : Stream
|
||||
{
|
||||
private readonly Stream stream;
|
||||
private bool isDisposed;
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading;
|
||||
|
||||
// Adapted from https://github.com/LogosBible/bsdiff.net/blob/master/src/bsdiff/BinaryPatchUtility.cs
|
||||
|
||||
namespace Squirrel.Bsdiff
|
||||
namespace Squirrel.Compression
|
||||
{
|
||||
/*
|
||||
The original bsdiff.c source code (http://www.daemonology.net/bsdiff/) is
|
||||
@@ -35,7 +36,7 @@ namespace Squirrel.Bsdiff
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
class BinaryPatchUtility
|
||||
public class BinaryPatchUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Squirrel
|
||||
namespace Squirrel.Compression
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an error that occurs when a package does not match it's expected SHA checksum
|
||||
@@ -1,21 +1,22 @@
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Squirrel.Bsdiff;
|
||||
using Squirrel.SimpleSplat;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Squirrel
|
||||
namespace Squirrel.Compression
|
||||
{
|
||||
internal class DeltaPackage : IEnableLogger
|
||||
public class DeltaPackage
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
private readonly string _baseTempDir;
|
||||
|
||||
public DeltaPackage(string baseTempDir = null)
|
||||
public DeltaPackage(ILogger logger, string baseTempDir = null)
|
||||
{
|
||||
_log = logger;
|
||||
_baseTempDir = baseTempDir ?? Utility.GetDefaultTempBaseDirectory();
|
||||
}
|
||||
|
||||
@@ -23,15 +24,16 @@ namespace Squirrel
|
||||
{
|
||||
progress = progress ?? (x => { });
|
||||
|
||||
Contract.Requires(deltaPackageZip != null);
|
||||
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
|
||||
if (deltaPackageZip is null) throw new ArgumentNullException(nameof(deltaPackageZip));
|
||||
if (String.IsNullOrEmpty(outputFile)) throw new ArgumentNullException(nameof(outputFile));
|
||||
if (File.Exists(outputFile)) throw new ArgumentException("File already exists", nameof(outputFile));
|
||||
|
||||
using (Utility.GetTempDirectory(out var deltaPath, _baseTempDir))
|
||||
using (Utility.GetTempDirectory(out var workingPath, _baseTempDir)) {
|
||||
EasyZip.ExtractZipToDirectory(deltaPackageZip, deltaPath);
|
||||
EasyZip.ExtractZipToDirectory(_log, deltaPackageZip, deltaPath);
|
||||
progress(25);
|
||||
|
||||
EasyZip.ExtractZipToDirectory(basePackageZip, workingPath);
|
||||
EasyZip.ExtractZipToDirectory(_log, basePackageZip, workingPath);
|
||||
progress(50);
|
||||
|
||||
var pathsVisited = new List<string>();
|
||||
@@ -59,7 +61,7 @@ namespace Squirrel
|
||||
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
|
||||
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
|
||||
.ForEach(x => {
|
||||
this.Log().Info("{0} was in old package but not in new one, deleting", x);
|
||||
_log.Info($"{x} was in old package but not in new one, deleting");
|
||||
File.Delete(Path.Combine(workingPath, x));
|
||||
});
|
||||
|
||||
@@ -70,13 +72,13 @@ namespace Squirrel
|
||||
deltaPathRelativePaths
|
||||
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ForEach(x => {
|
||||
this.Log().Info("Updating metadata file: {0}", x);
|
||||
_log.Info($"Updating metadata file: {x}");
|
||||
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
|
||||
});
|
||||
|
||||
this.Log().Info("Repacking into full package: {0}", outputFile);
|
||||
_log.Info($"Repacking into full package: {outputFile}");
|
||||
|
||||
EasyZip.CreateZipFromDirectory(outputFile, workingPath);
|
||||
EasyZip.CreateZipFromDirectory(_log, outputFile, workingPath);
|
||||
|
||||
progress(100);
|
||||
}
|
||||
@@ -88,10 +90,10 @@ namespace Squirrel
|
||||
{
|
||||
progress = progress ?? (x => { });
|
||||
|
||||
Contract.Requires(deltaPackageZip != null);
|
||||
if (deltaPackageZip is null) throw new ArgumentNullException(nameof(deltaPackageZip));
|
||||
|
||||
using var _1 = Utility.GetTempDirectory(out var deltaPath, _baseTempDir);
|
||||
EasyZip.ExtractZipToDirectory(deltaPackageZip, deltaPath);
|
||||
EasyZip.ExtractZipToDirectory(_log, deltaPackageZip, deltaPath);
|
||||
progress(10);
|
||||
|
||||
var pathsVisited = new List<string>();
|
||||
@@ -112,8 +114,8 @@ namespace Squirrel
|
||||
var file = files[index];
|
||||
pathsVisited.Add(Regex.Replace(file, @"\.(bs)?diff$", "").ToLowerInvariant());
|
||||
applyDiffToFile(deltaPath, file, workingPath);
|
||||
var perc = (index + 1) / (double)files.Length * 100;
|
||||
Utility.CalculateProgress((int)perc, 10, 90);
|
||||
var perc = (index + 1) / (double) files.Length * 100;
|
||||
Utility.CalculateProgress((int) perc, 10, 90);
|
||||
}
|
||||
|
||||
progress(90);
|
||||
@@ -124,7 +126,7 @@ namespace Squirrel
|
||||
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
|
||||
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
|
||||
.ForEach(x => {
|
||||
this.Log().Info("{0} was in old package but not in new one, deleting", x);
|
||||
_log.Info($"{x} was in old package but not in new one, deleting");
|
||||
File.Delete(Path.Combine(workingPath, x));
|
||||
});
|
||||
|
||||
@@ -135,7 +137,7 @@ namespace Squirrel
|
||||
deltaPathRelativePaths
|
||||
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ForEach(x => {
|
||||
this.Log().Info("Updating metadata file: {0}", x);
|
||||
_log.Info($"Updating metadata file: {x}");
|
||||
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
|
||||
});
|
||||
|
||||
@@ -151,20 +153,20 @@ namespace Squirrel
|
||||
|
||||
// NB: Zero-length diffs indicate the file hasn't actually changed
|
||||
if (new FileInfo(inputFile).Length == 0) {
|
||||
this.Log().Info("{0} exists unchanged, skipping", relativeFilePath);
|
||||
_log.Info($"{relativeFilePath} exists unchanged, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (relativeFilePath.EndsWith(".bsdiff", StringComparison.InvariantCultureIgnoreCase)) {
|
||||
using (var of = File.OpenWrite(tempTargetFile))
|
||||
using (var inf = File.OpenRead(finalTarget)) {
|
||||
this.Log().Info("Applying bsdiff to {0}", relativeFilePath);
|
||||
_log.Info($"Applying bsdiff to {relativeFilePath}");
|
||||
BinaryPatchUtility.Apply(inf, () => File.OpenRead(inputFile), of);
|
||||
}
|
||||
|
||||
verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile);
|
||||
} else if (relativeFilePath.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase)) {
|
||||
this.Log().Info("Applying msdiff to {0}", relativeFilePath);
|
||||
_log.Info($"Applying msdiff to {relativeFilePath}");
|
||||
|
||||
if (SquirrelRuntimeInfo.IsWindows) {
|
||||
MsDeltaCompression.ApplyDelta(inputFile, finalTarget, tempTargetFile);
|
||||
@@ -176,7 +178,7 @@ namespace Squirrel
|
||||
} else {
|
||||
using (var of = File.OpenWrite(tempTargetFile))
|
||||
using (var inf = File.OpenRead(inputFile)) {
|
||||
this.Log().Info("Adding new file: {0}", relativeFilePath);
|
||||
_log.Info($"Adding new file: {relativeFilePath}");
|
||||
inf.CopyTo(of);
|
||||
}
|
||||
}
|
||||
@@ -196,14 +198,12 @@ namespace Squirrel
|
||||
var actualReleaseEntry = ReleaseEntry.GenerateFromFile(tempTargetFile);
|
||||
|
||||
if (expectedReleaseEntry.Filesize != actualReleaseEntry.Filesize) {
|
||||
this.Log().Warn("Patched file {0} has incorrect size, expected {1}, got {2}", relativeFilePath,
|
||||
expectedReleaseEntry.Filesize, actualReleaseEntry.Filesize);
|
||||
_log.Warn($"Patched file {relativeFilePath} has incorrect size, expected {expectedReleaseEntry.Filesize}, got {actualReleaseEntry.Filesize}");
|
||||
throw new ChecksumFailedException() { Filename = relativeFilePath };
|
||||
}
|
||||
|
||||
if (expectedReleaseEntry.SHA1 != actualReleaseEntry.SHA1) {
|
||||
this.Log().Warn("Patched file {0} has incorrect SHA1, expected {1}, got {2}", relativeFilePath,
|
||||
expectedReleaseEntry.SHA1, actualReleaseEntry.SHA1);
|
||||
_log.Warn($"Patched file {relativeFilePath} has incorrect SHA1, expected {expectedReleaseEntry.SHA1}, got {actualReleaseEntry.SHA1}");
|
||||
throw new ChecksumFailedException() { Filename = relativeFilePath };
|
||||
}
|
||||
}
|
||||
41
src/Squirrel/Compression/EasyZip.cs
Normal file
41
src/Squirrel/Compression/EasyZip.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
using System;
|
||||
using System.IO.Compression;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Squirrel.Compression
|
||||
{
|
||||
public static class EasyZip
|
||||
{
|
||||
public static void ExtractZipToDirectory(ILogger logger, string inputFile, string outputDirectory)
|
||||
{
|
||||
logger.Info($"Extracting '{inputFile}' to '{outputDirectory}' using System.IO.Compression...");
|
||||
Utility.DeleteFileOrDirectoryHard(outputDirectory);
|
||||
ZipFile.ExtractToDirectory(inputFile, outputDirectory);
|
||||
}
|
||||
|
||||
public static void CreateZipFromDirectory(ILogger logger, string outputFile, string directoryToCompress)
|
||||
{
|
||||
logger.Info($"Compressing '{directoryToCompress}' to '{outputFile}' using System.IO.Compression...");
|
||||
ZipFile.CreateFromDirectory(directoryToCompress, outputFile);
|
||||
}
|
||||
|
||||
//private static void AddAllFromDirectoryInNestedDir(
|
||||
// IWritableArchive writableArchive,
|
||||
// string filePath, string searchPattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories)
|
||||
//{
|
||||
// var di = new DirectoryInfo(filePath);
|
||||
// var parent = di.Parent;
|
||||
|
||||
// using (writableArchive.PauseEntryRebuilding())
|
||||
// {
|
||||
// foreach (var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption))
|
||||
// {
|
||||
// var fileInfo = new FileInfo(path);
|
||||
// writableArchive.AddEntry(fileInfo.FullName.Substring(parent.FullName.Length), fileInfo.OpenRead(), true, fileInfo.Length,
|
||||
// fileInfo.LastWriteTime);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Squirrel
|
||||
namespace Squirrel.Compression
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal class MsDeltaCompression
|
||||
public class MsDeltaCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// The ApplyDelta function use the specified delta and source files to create a new copy of the target file.
|
||||
@@ -1,193 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using NuGet.Versioning;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies several common places where shortcuts can be installed on a user's system
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ShortcutLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// A shortcut in ProgramFiles within a publisher sub-directory
|
||||
/// </summary>
|
||||
StartMenu = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// A shortcut on the current user desktop
|
||||
/// </summary>
|
||||
Desktop = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// A shortcut in Startup/Run folder will cause the app to be automatially started on user login.
|
||||
/// </summary>
|
||||
Startup = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// A shortcut in the application folder, useful for portable applications.
|
||||
/// </summary>
|
||||
AppRoot = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// A shortcut in ProgramFiles root folder (not in a company/publisher sub-directory). This is commonplace as of more recent versions of windows.
|
||||
/// </summary>
|
||||
StartMenuRoot = 1 << 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the UpdateManager is used in a Install or Update scenario.
|
||||
/// </summary>
|
||||
public enum UpdaterIntention
|
||||
{
|
||||
/// <summary>
|
||||
/// The current intent is to perform a full app install, and overwrite or
|
||||
/// repair any app already installed of the same name.
|
||||
/// </summary>
|
||||
Install,
|
||||
|
||||
/// <summary>
|
||||
/// The current intent is to perform an app update, and to do nothing if there
|
||||
/// is no newer version available to install.
|
||||
/// </summary>
|
||||
Update
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides update functionality to applications, and general helper
|
||||
/// functions for managing installed shortcuts and registry entries. Use this
|
||||
/// to check if the current app is installed or not before performing an update.
|
||||
/// </summary>
|
||||
public interface IUpdateManager : IDisposable, IEnableLogger, IAppTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetch the remote store for updates and compare against the current
|
||||
/// version to determine what updates to download.
|
||||
/// </summary>
|
||||
/// <param name="intention">Indicates whether the UpdateManager is used
|
||||
/// in a Install or Update scenario.</param>
|
||||
/// <param name="ignoreDeltaUpdates">Set this flag if applying a release
|
||||
/// fails to fall back to a full release, which takes longer to download
|
||||
/// but is less error-prone.</param>
|
||||
/// <param name="progress">A Observer which can be used to report Progress -
|
||||
/// will return values from 0-100 and Complete, or Throw</param>
|
||||
/// <returns>An UpdateInfo object representing the updates to install.
|
||||
/// </returns>
|
||||
Task<UpdateInfo> CheckForUpdate(bool ignoreDeltaUpdates = false, Action<int> progress = null, UpdaterIntention intention = UpdaterIntention.Update);
|
||||
|
||||
/// <summary>
|
||||
/// Download a list of releases into the local package directory.
|
||||
/// </summary>
|
||||
/// <param name="releasesToDownload">The list of releases to download,
|
||||
/// almost always from UpdateInfo.ReleasesToApply.</param>
|
||||
/// <param name="progress">A Observer which can be used to report Progress -
|
||||
/// will return values from 0-100 and Complete, or Throw</param>
|
||||
/// <returns>A completion Observable - either returns a single
|
||||
/// Unit.Default then Complete, or Throw</returns>
|
||||
Task DownloadReleases(IEnumerable<ReleaseEntry> releasesToDownload, Action<int> progress = null);
|
||||
|
||||
/// <summary>
|
||||
/// Take an already downloaded set of releases and apply them,
|
||||
/// copying in the new files from the NuGet package and rewriting
|
||||
/// the application shortcuts.
|
||||
/// </summary>
|
||||
/// <param name="updateInfo">The UpdateInfo instance acquired from
|
||||
/// CheckForUpdate</param>
|
||||
/// <param name="progress">A Observer which can be used to report Progress -
|
||||
/// will return values from 0-100 and Complete, or Throw</param>
|
||||
/// <returns>The path to the installed application (i.e. the path where
|
||||
/// your package's contents ended up</returns>
|
||||
Task<string> ApplyReleases(UpdateInfo updateInfo, Action<int> progress = null);
|
||||
|
||||
/// <summary>
|
||||
/// This will check for updates, download any new available updates, and apply those
|
||||
/// updates in a single step. The same task can be accomplished by using <see cref="IUpdateManager.CheckForUpdate"/>,
|
||||
/// followed by <see cref="IUpdateManager.DownloadReleases"/> and <see cref="IUpdateManager.ApplyReleases"/>.
|
||||
/// </summary>
|
||||
/// <returns>The installed update, or null if there were no updates available</returns>
|
||||
Task<ReleaseEntry> UpdateApp(Action<int> progress = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides accessory functions such as managing uninstall registry or
|
||||
/// creating, updating, and removing shortcuts.
|
||||
/// </summary>
|
||||
public interface IAppTools
|
||||
{
|
||||
/// <summary>True if the current executable is inside the target <see cref="AppDirectory"/>.</summary>
|
||||
bool IsInstalledApp { get; }
|
||||
|
||||
/// <summary>The directory the app is (or will be) installed in.</summary>
|
||||
string AppDirectory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently installed version of the given executable, or if
|
||||
/// not given, the currently running assembly
|
||||
/// </summary>
|
||||
/// <returns>The running version, or null if this is not a Squirrel
|
||||
/// installed app (i.e. you're running from VS)</returns>
|
||||
SemanticVersion CurrentlyInstalledVersion();
|
||||
|
||||
/// <summary>
|
||||
/// Create a shortcut on the Desktop / Start Menu for the given
|
||||
/// executable. Metadata from the currently installed NuGet package
|
||||
/// and information from the Version Header of the EXE will be used
|
||||
/// to construct the shortcut folder / name.
|
||||
/// </summary>
|
||||
/// <param name="exeName">The name of the executable, relative to the
|
||||
/// app install directory.</param>
|
||||
/// <param name="locations">The locations to install the shortcut</param>
|
||||
/// <param name="updateOnly">Set to false during initial install, true
|
||||
/// during app update.</param>
|
||||
/// <param name="programArguments">The arguments to code into the shortcut</param>
|
||||
/// <param name="icon">The shortcut icon</param>
|
||||
void CreateShortcutsForExecutable(string exeName, ShortcutLocation locations, bool updateOnly, string programArguments, string icon);
|
||||
|
||||
/// <summary>
|
||||
/// Removes shortcuts created by CreateShortcutsForExecutable
|
||||
/// </summary>
|
||||
/// <param name="exeName">The name of the executable, relative to the
|
||||
/// app install directory.</param>
|
||||
/// <param name="locations">The locations to install the shortcut</param>
|
||||
void RemoveShortcutsForExecutable(string exeName, ShortcutLocation locations);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="IUpdateManager"/> which provide simplified functionality
|
||||
/// </summary>
|
||||
public static class EasyModeMixin
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a shortcut to the currently running executable at the specified locations.
|
||||
/// See <see cref="IAppTools.CreateShortcutsForExecutable"/> to create a shortcut to a different program
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static void CreateShortcutForThisExe(this IAppTools This, ShortcutLocation location = ShortcutLocation.Desktop | ShortcutLocation.StartMenu)
|
||||
{
|
||||
This.CreateShortcutsForExecutable(
|
||||
Path.GetFileName(SquirrelRuntimeInfo.EntryExePath),
|
||||
location,
|
||||
Environment.CommandLine.Contains("squirrel-install") == false,
|
||||
null, // shortcut arguments
|
||||
null); // shortcut icon
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a shortcut for the currently running executable at the specified locations.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static void RemoveShortcutForThisExe(this IAppTools This, ShortcutLocation location = ShortcutLocation.Desktop | ShortcutLocation.StartMenu)
|
||||
{
|
||||
This.RemoveShortcutsForExecutable(
|
||||
Path.GetFileName(SquirrelRuntimeInfo.EntryExePath),
|
||||
location);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
internal static class EasyZip
|
||||
{
|
||||
private static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(EasyZip));
|
||||
|
||||
public static void ExtractZipToDirectory(string inputFile, string outputDirectory)
|
||||
{
|
||||
Log.Info($"Extracting '{inputFile}' to '{outputDirectory}' using System.IO.Compression...");
|
||||
Utility.DeleteFileOrDirectoryHard(outputDirectory);
|
||||
ZipFile.ExtractToDirectory(inputFile, outputDirectory);
|
||||
}
|
||||
|
||||
public static void CreateZipFromDirectory(string outputFile, string directoryToCompress, bool nestDirectory = false)
|
||||
{
|
||||
if (nestDirectory) {
|
||||
throw new NotImplementedException();
|
||||
//AddAllFromDirectoryInNestedDir(archive, directoryToCompress);
|
||||
} else {
|
||||
Log.Info($"Compressing '{directoryToCompress}' to '{outputFile}' using System.IO.Compression...");
|
||||
ZipFile.CreateFromDirectory(directoryToCompress, outputFile);
|
||||
}
|
||||
}
|
||||
|
||||
//private static void AddAllFromDirectoryInNestedDir(
|
||||
// IWritableArchive writableArchive,
|
||||
// string filePath, string searchPattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories)
|
||||
//{
|
||||
// var di = new DirectoryInfo(filePath);
|
||||
// var parent = di.Parent;
|
||||
|
||||
// using (writableArchive.PauseEntryRebuilding())
|
||||
// {
|
||||
// foreach (var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption))
|
||||
// {
|
||||
// var fileInfo = new FileInfo(path);
|
||||
// writableArchive.AddEntry(fileInfo.FullName.Substring(parent.FullName.Length), fileInfo.OpenRead(), true, fileInfo.Length,
|
||||
// fileInfo.LastWriteTime);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
public static bool IsDirectory(this ZipArchiveEntry entry)
|
||||
{
|
||||
return entry.FullName.EndsWith("/") || entry.FullName.EndsWith("\\") || String.IsNullOrEmpty(entry.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,12 @@ using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel.SimpleSplat;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
internal static class PlatformUtil
|
||||
{
|
||||
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(PlatformUtil));
|
||||
|
||||
private const string OSX_CSTD_LIB = "libSystem.dylib";
|
||||
private const string NIX_CSTD_LIB = "libc";
|
||||
private const string WIN_KERNEL32 = "kernel32.dll";
|
||||
@@ -24,84 +22,6 @@ namespace Squirrel
|
||||
private const string WIN_NTDLL = "NTDLL.DLL";
|
||||
private const string WIN_PSAPI = "psapi.dll";
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
[DllImport(NIX_CSTD_LIB, EntryPoint = "getppid")]
|
||||
private static extern int nix_getppid();
|
||||
|
||||
[SupportedOSPlatform("osx")]
|
||||
[DllImport(OSX_CSTD_LIB, EntryPoint = "getppid")]
|
||||
private static extern int osx_getppid();
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_KERNEL32)]
|
||||
private static extern IntPtr GetCurrentProcess();
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[DllImport(WIN_NTDLL, SetLastError = true)]
|
||||
private static extern int NtQueryInformationProcess(IntPtr hProcess, int pic, ref PROCESS_BASIC_INFORMATION pbi, int cb, out int pSize);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private struct PROCESS_BASIC_INFORMATION
|
||||
{
|
||||
public nint ExitStatus;
|
||||
public nint PebBaseAddress;
|
||||
public nint AffinityMask;
|
||||
public nint BasePriority;
|
||||
public nuint UniqueProcessId;
|
||||
public nint InheritedFromUniqueProcessId;
|
||||
}
|
||||
|
||||
public static Process GetParentProcess()
|
||||
{
|
||||
int parentId;
|
||||
|
||||
if (SquirrelRuntimeInfo.IsWindows) {
|
||||
var pbi = new PROCESS_BASIC_INFORMATION();
|
||||
NtQueryInformationProcess(GetCurrentProcess(), 0, ref pbi, Marshal.SizeOf(typeof(PROCESS_BASIC_INFORMATION)), out _);
|
||||
parentId = (int) pbi.InheritedFromUniqueProcessId;
|
||||
} else if (SquirrelRuntimeInfo.IsLinux) {
|
||||
parentId = nix_getppid();
|
||||
} else if (SquirrelRuntimeInfo.IsOSX) {
|
||||
parentId = osx_getppid();
|
||||
} else {
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
// the parent process has exited (nix/osx)
|
||||
if (parentId <= 1)
|
||||
return null;
|
||||
|
||||
try {
|
||||
var p = Process.GetProcessById(parentId);
|
||||
|
||||
// the retrieved process is not our parent, the pid has been reused
|
||||
if (p.StartTime > Process.GetCurrentProcess().StartTime)
|
||||
return null;
|
||||
|
||||
return p;
|
||||
} catch (ArgumentException) {
|
||||
// the process has exited (windows)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void WaitForParentProcessToExit()
|
||||
{
|
||||
var p = GetParentProcess();
|
||||
if (p == null) {
|
||||
Log.Warn("Will not wait. Parent process has already exited.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Info($"Waiting for PID {p.Id} to exit (60s timeout)...");
|
||||
var exited = p.WaitForExit(60_000);
|
||||
if (!exited) {
|
||||
throw new Exception("Parent wait timed out.");
|
||||
}
|
||||
|
||||
Log.Info($"PID {p.Id} has exited.");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("osx")]
|
||||
[DllImport(OSX_CSTD_LIB, EntryPoint = "chmod", SetLastError = true)]
|
||||
private static extern int osx_chmod(string pathname, int mode);
|
||||
@@ -257,14 +177,14 @@ namespace Squirrel
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static void KillProcessesInDirectory(string directoryToKill)
|
||||
public static void KillProcessesInDirectory(ILogger logger, string directoryToKill)
|
||||
{
|
||||
Log.Info("Killing all processes in " + directoryToKill);
|
||||
logger.Info("Killing all processes in " + directoryToKill);
|
||||
var myPid = Process.GetCurrentProcess().Id;
|
||||
int c = 0;
|
||||
foreach (var x in GetRunningProcessesInDirectory(directoryToKill)) {
|
||||
if (myPid == x.ProcessId) {
|
||||
Log.Info($"Skipping '{x.ProcessExePath}' (is current process)");
|
||||
logger.Info($"Skipping '{x.ProcessExePath}' (is current process)");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -272,11 +192,11 @@ namespace Squirrel
|
||||
Process.GetProcessById(x.ProcessId).Kill();
|
||||
c++;
|
||||
} catch (Exception ex) {
|
||||
Log.WarnException($"Unable to terminate process (pid.{x.ProcessId})", ex);
|
||||
logger.Warn(ex, $"Unable to terminate process (pid.{x.ProcessId})");
|
||||
}
|
||||
}
|
||||
|
||||
Log.Info($"Terminated {c} processes successfully.");
|
||||
logger.Info($"Terminated {c} processes successfully.");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Squirrel.Lib
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal class ResourceReader : IDisposable
|
||||
{
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern IntPtr LoadLibraryEx(string lpModuleName, IntPtr hFile, uint dwFlags);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern IntPtr FindResource(IntPtr hModule, string lpName, string lpType);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, IntPtr lpType);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, string lpType);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern IntPtr FindResourceEx(IntPtr hModule, IntPtr lpType, IntPtr lpName, ushort wLanguage);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern IntPtr FindResourceEx(IntPtr hModule, string lpType, IntPtr lpName, ushort wLanguage);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern IntPtr FindResourceEx(IntPtr hModule, string lpType, string lpName, ushort wLanguage);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint SizeofResource(IntPtr hModule, IntPtr handle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr LoadResource(IntPtr hModule, IntPtr handle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr LockResource(IntPtr hglobal);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool FreeLibrary(IntPtr hModule);
|
||||
|
||||
private IntPtr hModule;
|
||||
const uint LOAD_LIBRARY_AS_DATAFILE = 2;
|
||||
private bool _disposed;
|
||||
|
||||
public ResourceReader(string peFile)
|
||||
{
|
||||
hModule = LoadLibraryEx(peFile, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE);
|
||||
if (hModule == IntPtr.Zero) {
|
||||
throw new Win32Exception();
|
||||
}
|
||||
}
|
||||
|
||||
~ResourceReader()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public byte[] ReadResource(string resourceType, string resourceName)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
||||
|
||||
var hResource = FindResource(hModule, resourceName, resourceType);
|
||||
if (hResource == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
return ReadResourceToBytes(hResource);
|
||||
}
|
||||
|
||||
public byte[] ReadResource(string resourceType, string resourceName, ushort lang)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
||||
|
||||
var hResource = FindResourceEx(hModule, resourceType, resourceName, lang);
|
||||
if (hResource == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
return ReadResourceToBytes(hResource);
|
||||
}
|
||||
|
||||
public byte[] ReadResource(string resourceType, IntPtr resourceName)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
||||
|
||||
var hResource = FindResource(hModule, resourceName, resourceType);
|
||||
if (hResource == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
return ReadResourceToBytes(hResource);
|
||||
}
|
||||
|
||||
public byte[] ReadResource(string resourceType, IntPtr resourceName, ushort lang)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
||||
|
||||
var hResource = FindResourceEx(hModule, resourceType, resourceName, lang);
|
||||
if (hResource == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
return ReadResourceToBytes(hResource);
|
||||
}
|
||||
|
||||
public byte[] ReadResource(IntPtr resourceType, IntPtr resourceName)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
||||
|
||||
var hResource = FindResource(hModule, resourceName, resourceType);
|
||||
if (hResource == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
return ReadResourceToBytes(hResource);
|
||||
}
|
||||
|
||||
public byte[] ReadResource(IntPtr resourceType, IntPtr resourceName, ushort lang)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(ResourceReader));
|
||||
|
||||
var hResource = FindResourceEx(hModule, resourceType, resourceName, lang);
|
||||
if (hResource == IntPtr.Zero)
|
||||
return null;
|
||||
|
||||
return ReadResourceToBytes(hResource);
|
||||
}
|
||||
|
||||
private byte[] ReadResourceToBytes(IntPtr hResource)
|
||||
{
|
||||
uint size = SizeofResource(hModule, hResource);
|
||||
if (size == 0)
|
||||
throw new Win32Exception();
|
||||
|
||||
var hGlobal = LoadResource(hModule, hResource);
|
||||
if (hGlobal == IntPtr.Zero)
|
||||
throw new Win32Exception();
|
||||
|
||||
var data = LockResource(hGlobal);
|
||||
if (data == IntPtr.Zero)
|
||||
throw new Win32Exception(0x21);
|
||||
|
||||
var buf = new byte[size];
|
||||
Marshal.Copy(data, buf, 0, (int) size);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public byte[] ReadAssemblyManifest()
|
||||
{
|
||||
return ReadResource(new IntPtr(24) /*RT_MANIFEST*/, new IntPtr(1));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed) {
|
||||
_disposed = true;
|
||||
FreeLibrary(hModule);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
internal sealed class SingleGlobalInstance : IDisposable, IEnableLogger
|
||||
{
|
||||
IDisposable handle = null;
|
||||
|
||||
public SingleGlobalInstance(string key, TimeSpan timeOut)
|
||||
{
|
||||
if (ModeDetector.InUnitTestRunner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var path = Path.Combine(Path.GetTempPath(), ".squirrel-lock-" + key);
|
||||
|
||||
var st = new Stopwatch();
|
||||
st.Start();
|
||||
|
||||
var fh = default(FileStream);
|
||||
while (st.Elapsed < timeOut) {
|
||||
try {
|
||||
fh = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Delete);
|
||||
fh.Write(new byte[] { 0xba, 0xad, 0xf0, 0x0d, }, 0, 4);
|
||||
break;
|
||||
} catch (Exception ex) {
|
||||
this.Log().WarnException("Failed to grab lockfile, will retry: " + path, ex);
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
}
|
||||
|
||||
st.Stop();
|
||||
|
||||
if (fh == null) {
|
||||
throw new Exception("Couldn't acquire lock, is another instance running?");
|
||||
}
|
||||
|
||||
handle = Disposable.Create(() => {
|
||||
fh.Dispose();
|
||||
File.Delete(path);
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (ModeDetector.InUnitTestRunner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var disp = Interlocked.Exchange(ref handle, null);
|
||||
if (disp != null) disp.Dispose();
|
||||
}
|
||||
|
||||
~SingleGlobalInstance()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using Squirrel.Lib;
|
||||
using Squirrel.NuGet;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
internal static class SquirrelAwareExecutableDetector
|
||||
{
|
||||
const string SQUIRREL_AWARE_KEY = "SquirrelAwareVersion";
|
||||
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(SquirrelAwareExecutableDetector));
|
||||
|
||||
public static List<string> GetAllSquirrelAwareApps(string directory, int minimumVersion = 1)
|
||||
{
|
||||
var di = new DirectoryInfo(directory);
|
||||
|
||||
return di.EnumerateFiles()
|
||||
.Where(x => Utility.FileHasExtension(x.Name, ".exe"))
|
||||
.Select(x => x.FullName)
|
||||
.Where(x => (GetSquirrelAwareVersion(x) ?? -1) >= minimumVersion)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static int? GetSquirrelAwareVersion(string exePath)
|
||||
{
|
||||
if (!File.Exists(exePath)) return null;
|
||||
var fullname = Path.GetFullPath(exePath);
|
||||
|
||||
// ways to search for SquirrelAwareVersion, ordered by precedence
|
||||
// search exe-embedded values first, and if not found, move on to sidecar files
|
||||
var detectors = new List<Func<string, int?>>();
|
||||
|
||||
if (SquirrelRuntimeInfo.IsWindows) {
|
||||
detectors.Add(GetEmbeddedManifestSquirrelAwareValue);
|
||||
detectors.Add(GetVersionBlockSquirrelAwareValue);
|
||||
} else {
|
||||
Log.Warn("Disabled embedded manifest SquirrelAware detection (only supported on windows).");
|
||||
}
|
||||
|
||||
detectors.Add(GetSidecarSquirrelAwareValue);
|
||||
detectors.Add(GetSideBySideManifestSquirrelAwareValue);
|
||||
detectors.Add(GetSideBySideDllManifestSquirrelAwareValue);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
bool error = false;
|
||||
foreach (var fn in detectors) {
|
||||
try {
|
||||
var v = fn(exePath);
|
||||
if (v != null) return v;
|
||||
} catch {
|
||||
error = true;
|
||||
// do not throw, otherwise other detectors will not run
|
||||
}
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
// we tried all the detectors and none of them threw, so we don't need to retry
|
||||
break;
|
||||
}
|
||||
|
||||
// retry 3 times with 100ms delay
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
static int? GetVersionBlockSquirrelAwareValue(string executable)
|
||||
{
|
||||
return StringFileInfo.ReadVersionInfo(executable, out var vi)
|
||||
.Where(i => i.Key == SQUIRREL_AWARE_KEY)
|
||||
.Where(i => int.TryParse(i.Value, out var _))
|
||||
.Select(i => (int?) int.Parse(i.Value))
|
||||
.FirstOrDefault(i => i > 0);
|
||||
}
|
||||
|
||||
static int? GetSidecarSquirrelAwareValue(string executable)
|
||||
{
|
||||
// Looks for a "MyApp.exe.squirrel" sidecar file
|
||||
// the file should contain just the integer version (eg. "1")
|
||||
var sidecarPath = executable + ".squirrel";
|
||||
if (File.Exists(sidecarPath)) {
|
||||
var txt = File.ReadAllText(sidecarPath);
|
||||
if (int.TryParse(txt, out var pv)) {
|
||||
return pv;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static int? GetSideBySideManifestSquirrelAwareValue(string executable)
|
||||
{
|
||||
// Looks for an external application manifest eg. "MyApp.exe.manifest"
|
||||
var manifestPath = executable + ".manifest";
|
||||
if (File.Exists(manifestPath)) {
|
||||
return ParseManifestAwareValue(File.ReadAllBytes(manifestPath));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static int? GetSideBySideDllManifestSquirrelAwareValue(string executable)
|
||||
{
|
||||
// Looks for an external application DLL manifest eg. "MyApp.dll.manifest"
|
||||
var manifestPath = Path.Combine(
|
||||
Path.GetDirectoryName(executable),
|
||||
Path.GetFileNameWithoutExtension(executable) + ".dll.manifest");
|
||||
if (File.Exists(manifestPath)) {
|
||||
return ParseManifestAwareValue(File.ReadAllBytes(manifestPath));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
static int? GetEmbeddedManifestSquirrelAwareValue(string executable)
|
||||
{
|
||||
// Looks for an embedded application manifest
|
||||
byte[] buffer = null;
|
||||
using (var rr = new ResourceReader(executable))
|
||||
buffer = rr.ReadAssemblyManifest();
|
||||
return ParseManifestAwareValue(buffer);
|
||||
}
|
||||
|
||||
static int? ParseManifestAwareValue(byte[] buffer)
|
||||
{
|
||||
if (buffer == null)
|
||||
return null;
|
||||
|
||||
var document = XDocument.Load(new MemoryStream(buffer));
|
||||
var aware = document.Root.ElementsNoNamespace(SQUIRREL_AWARE_KEY).FirstOrDefault();
|
||||
if (aware != null && int.TryParse(aware.Value, out var pv)) {
|
||||
return pv;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
internal class SubStream : Stream
|
||||
{
|
||||
private readonly Stream _wrappedStream;
|
||||
private readonly long _startOffset;
|
||||
|
||||
public SubStream(Stream wrappedStream, long startOffset)
|
||||
{
|
||||
_wrappedStream = wrappedStream;
|
||||
_startOffset = startOffset;
|
||||
|
||||
if (startOffset >= wrappedStream.Length)
|
||||
throw new ArgumentException("Offset+Length must be less than or equal to the length of the wrapped stream");
|
||||
|
||||
Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
public override bool CanRead => _wrappedStream.CanRead;
|
||||
|
||||
public override bool CanSeek => _wrappedStream.CanSeek;
|
||||
|
||||
public override bool CanWrite => _wrappedStream.CanWrite;
|
||||
|
||||
public override long Length => _wrappedStream.Length - _startOffset;
|
||||
|
||||
public override long Position {
|
||||
get => _wrappedStream.Position - _startOffset;
|
||||
set => Seek(value, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
_wrappedStream.Flush();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return _wrappedStream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
if (offset > Length)
|
||||
throw new ArgumentException("Offset can not be greater than stream length");
|
||||
|
||||
if (origin == SeekOrigin.Begin) {
|
||||
return _wrappedStream.Seek(_startOffset + offset, SeekOrigin.Begin) - _startOffset;
|
||||
} else if (origin == SeekOrigin.End) {
|
||||
return _wrappedStream.Seek(offset, SeekOrigin.End) - _startOffset;
|
||||
}
|
||||
|
||||
var newPosition = _wrappedStream.Seek(offset, SeekOrigin.Current);
|
||||
if (newPosition < _startOffset)
|
||||
throw new ArgumentException("Cannot seak beyond the beginning of a stream");
|
||||
|
||||
return newPosition - _startOffset;
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
_wrappedStream.Write(buffer, offset, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
@@ -8,8 +8,8 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Squirrel.NuGet;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
@@ -180,7 +180,7 @@ namespace Squirrel
|
||||
Contract.Requires(!String.IsNullOrEmpty(to));
|
||||
|
||||
if (!File.Exists(from)) {
|
||||
Log().Warn("The file {0} does not exist", from);
|
||||
//Log().Warn("The file {0} does not exist", from);
|
||||
|
||||
// TODO: should we fail this operation?
|
||||
return;
|
||||
@@ -198,7 +198,7 @@ namespace Squirrel
|
||||
}, retries, retryDelay);
|
||||
}
|
||||
|
||||
public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250)
|
||||
public static T Retry<T>(this Func<T> block, int retries = 4, int retryDelay = 250, ILogger logger = null)
|
||||
{
|
||||
Contract.Requires(retries > 0);
|
||||
|
||||
@@ -208,7 +208,7 @@ namespace Squirrel
|
||||
return ret;
|
||||
} catch (Exception ex) {
|
||||
if (retries == 0) throw;
|
||||
Log().Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
|
||||
logger?.Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
|
||||
retries--;
|
||||
Thread.Sleep(retryDelay);
|
||||
}
|
||||
@@ -223,14 +223,14 @@ namespace Squirrel
|
||||
}, retries, retryDelay);
|
||||
}
|
||||
|
||||
public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250)
|
||||
public static async Task<T> RetryAsync<T>(this Func<Task<T>> block, int retries = 4, int retryDelay = 250, ILogger logger = null)
|
||||
{
|
||||
while (true) {
|
||||
try {
|
||||
return await block().ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
if (retries == 0) throw;
|
||||
Log().Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
|
||||
logger?.Warn($"Operation failed ({ex.Message}). Retrying {retries} more times...");
|
||||
retries--;
|
||||
await Task.Delay(retryDelay).ConfigureAwait(false);
|
||||
}
|
||||
@@ -346,14 +346,14 @@ namespace Squirrel
|
||||
/// <param name="throwOnFailure">Whether this function should throw if the delete fails.</param>
|
||||
/// <param name="renameFirst">Try to rename this object first before deleting. Can help prevent partial delete of folders.</param>
|
||||
/// <returns>True if the file system object was deleted, false otherwise.</returns>
|
||||
public static bool DeleteFileOrDirectoryHard(string path, bool throwOnFailure = true, bool renameFirst = false)
|
||||
public static bool DeleteFileOrDirectoryHard(string path, bool throwOnFailure = true, bool renameFirst = false, ILogger logger = null)
|
||||
{
|
||||
Contract.Requires(!String.IsNullOrEmpty(path));
|
||||
Log().Debug("Starting to delete: {0}", path);
|
||||
logger?.Debug($"Starting to delete: {path}");
|
||||
|
||||
try {
|
||||
if (File.Exists(path)) {
|
||||
DeleteFsiVeryHard(new FileInfo(path));
|
||||
DeleteFsiVeryHard(new FileInfo(path), logger);
|
||||
} else if (Directory.Exists(path)) {
|
||||
if (renameFirst) {
|
||||
// if there are locked files in a directory, we will not attempt to delte it
|
||||
@@ -362,26 +362,26 @@ namespace Squirrel
|
||||
path = oldPath;
|
||||
}
|
||||
|
||||
DeleteFsiTree(new DirectoryInfo(path));
|
||||
DeleteFsiTree(new DirectoryInfo(path), logger);
|
||||
} else {
|
||||
if (throwOnFailure)
|
||||
Log().Warn($"Cannot delete '{path}' if it does not exist.");
|
||||
logger?.Warn($"Cannot delete '{path}' if it does not exist.");
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
Log().ErrorException($"Unable to delete '{path}'", ex);
|
||||
logger?.Error(ex, $"Unable to delete '{path}'");
|
||||
if (throwOnFailure)
|
||||
throw;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteFsiTree(FileSystemInfo fileSystemInfo)
|
||||
private static void DeleteFsiTree(FileSystemInfo fileSystemInfo, ILogger logger)
|
||||
{
|
||||
// if junction / symlink, don't iterate, just delete it.
|
||||
if (fileSystemInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) {
|
||||
DeleteFsiVeryHard(fileSystemInfo);
|
||||
DeleteFsiVeryHard(fileSystemInfo, logger);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -390,19 +390,19 @@ namespace Squirrel
|
||||
var directoryInfo = fileSystemInfo as DirectoryInfo;
|
||||
if (directoryInfo != null) {
|
||||
foreach (FileSystemInfo childInfo in directoryInfo.GetFileSystemInfos()) {
|
||||
DeleteFsiTree(childInfo);
|
||||
DeleteFsiTree(childInfo, logger);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log().WarnException($"Unable to traverse children of '{fileSystemInfo.FullName}'", ex);
|
||||
logger?.Warn(ex, $"Unable to traverse children of '{fileSystemInfo.FullName}'");
|
||||
}
|
||||
|
||||
// finally, delete myself, we should try this even if deleting children failed
|
||||
// because Directory.Delete can also be recursive
|
||||
DeleteFsiVeryHard(fileSystemInfo);
|
||||
DeleteFsiVeryHard(fileSystemInfo, logger);
|
||||
}
|
||||
|
||||
private static void DeleteFsiVeryHard(FileSystemInfo fileSystemInfo)
|
||||
private static void DeleteFsiVeryHard(FileSystemInfo fileSystemInfo, ILogger logger)
|
||||
{
|
||||
// don't try to delete the running process
|
||||
if (FullPathEquals(fileSystemInfo.FullName, SquirrelRuntimeInfo.EntryExePath))
|
||||
@@ -429,7 +429,7 @@ namespace Squirrel
|
||||
}
|
||||
}, retries: 4, retryDelay: 50);
|
||||
} catch (Exception ex) {
|
||||
Log().WarnException($"Unable to delete child '{fileSystemInfo.FullName}'", ex);
|
||||
logger?.Warn(ex, $"Unable to delete child '{fileSystemInfo.FullName}'");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -539,138 +539,6 @@ namespace Squirrel
|
||||
return relativePath.Split(Path.DirectorySeparatorChar).Length == 4;
|
||||
}
|
||||
|
||||
public static void LogIfThrows(this IFullLogger This, LogLevel level, string message, Action block)
|
||||
{
|
||||
try {
|
||||
block();
|
||||
} catch (Exception ex) {
|
||||
switch (level) {
|
||||
case LogLevel.Debug:
|
||||
This.DebugException(message ?? "", ex);
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
This.InfoException(message ?? "", ex);
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
This.WarnException(message ?? "", ex);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
This.ErrorException(message ?? "", ex);
|
||||
break;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task LogIfThrows(this IFullLogger This, LogLevel level, string message, Func<Task> block)
|
||||
{
|
||||
try {
|
||||
await block().ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
switch (level) {
|
||||
case LogLevel.Debug:
|
||||
This.DebugException(message ?? "", ex);
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
This.InfoException(message ?? "", ex);
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
This.WarnException(message ?? "", ex);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
This.ErrorException(message ?? "", ex);
|
||||
break;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<T> LogIfThrows<T>(this IFullLogger This, LogLevel level, string message, Func<Task<T>> block)
|
||||
{
|
||||
try {
|
||||
return await block().ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
switch (level) {
|
||||
case LogLevel.Debug:
|
||||
This.DebugException(message ?? "", ex);
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
This.InfoException(message ?? "", ex);
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
This.WarnException(message ?? "", ex);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
This.ErrorException(message ?? "", ex);
|
||||
break;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static void WarnIfThrows(this IEnableLogger This, Action block, string message = null)
|
||||
{
|
||||
This.Log().LogIfThrows(LogLevel.Warn, message, block);
|
||||
}
|
||||
|
||||
public static Task WarnIfThrows(this IEnableLogger This, Func<Task> block, string message = null)
|
||||
{
|
||||
return This.Log().LogIfThrows(LogLevel.Warn, message, block);
|
||||
}
|
||||
|
||||
public static Task<T> WarnIfThrows<T>(this IEnableLogger This, Func<Task<T>> block, string message = null)
|
||||
{
|
||||
return This.Log().LogIfThrows(LogLevel.Warn, message, block);
|
||||
}
|
||||
|
||||
public static void ErrorIfThrows(this IEnableLogger This, Action block, string message = null)
|
||||
{
|
||||
This.Log().LogIfThrows(LogLevel.Error, message, block);
|
||||
}
|
||||
|
||||
public static Task ErrorIfThrows(this IEnableLogger This, Func<Task> block, string message = null)
|
||||
{
|
||||
return This.Log().LogIfThrows(LogLevel.Error, message, block);
|
||||
}
|
||||
|
||||
public static Task<T> ErrorIfThrows<T>(this IEnableLogger This, Func<Task<T>> block, string message = null)
|
||||
{
|
||||
return This.Log().LogIfThrows(LogLevel.Error, message, block);
|
||||
}
|
||||
|
||||
public static void WarnIfThrows(this IFullLogger This, Action block, string message = null)
|
||||
{
|
||||
This.LogIfThrows(LogLevel.Warn, message, block);
|
||||
}
|
||||
|
||||
public static Task WarnIfThrows(this IFullLogger This, Func<Task> block, string message = null)
|
||||
{
|
||||
return This.LogIfThrows(LogLevel.Warn, message, block);
|
||||
}
|
||||
|
||||
public static Task<T> WarnIfThrows<T>(this IFullLogger This, Func<Task<T>> block, string message = null)
|
||||
{
|
||||
return This.LogIfThrows(LogLevel.Warn, message, block);
|
||||
}
|
||||
|
||||
public static void ErrorIfThrows(this IFullLogger This, Action block, string message = null)
|
||||
{
|
||||
This.LogIfThrows(LogLevel.Error, message, block);
|
||||
}
|
||||
|
||||
public static Task ErrorIfThrows(this IFullLogger This, Func<Task> block, string message = null)
|
||||
{
|
||||
return This.LogIfThrows(LogLevel.Error, message, block);
|
||||
}
|
||||
|
||||
public static Task<T> ErrorIfThrows<T>(this IFullLogger This, Func<Task<T>> block, string message = null)
|
||||
{
|
||||
return This.LogIfThrows(LogLevel.Error, message, block);
|
||||
}
|
||||
|
||||
public static void ConsoleWriteWithColor(string text, ConsoleColor color)
|
||||
{
|
||||
var fc = Console.ForegroundColor;
|
||||
@@ -679,14 +547,6 @@ namespace Squirrel
|
||||
Console.ForegroundColor = fc;
|
||||
}
|
||||
|
||||
static IFullLogger logger;
|
||||
|
||||
static IFullLogger Log()
|
||||
{
|
||||
return logger ??
|
||||
(logger = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(Utility)));
|
||||
}
|
||||
|
||||
public static Guid CreateGuidFromHash(string text)
|
||||
{
|
||||
return CreateGuidFromHash(text, Utility.IsoOidNamespace);
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
// File: RollThroughLibrary/CreateMaps/JunctionPoint.cs
|
||||
// User: Adrian Hum/
|
||||
//
|
||||
// Created: 2017-11-19 2:46 PM
|
||||
// Modified: 2017-11-19 6:10 PM
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace Squirrel.Lib
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to NTFS junction points in .Net.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal static class JunctionPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The file or directory is not a reparse point.
|
||||
/// </summary>
|
||||
private const int ERROR_NOT_A_REPARSE_POINT = 4390;
|
||||
|
||||
/// <summary>
|
||||
/// The reparse point attribute cannot be set because it conflicts with an existing attribute.
|
||||
/// </summary>
|
||||
private const int ERROR_REPARSE_ATTRIBUTE_CONFLICT = 4391;
|
||||
|
||||
/// <summary>
|
||||
/// The data present in the reparse point buffer is invalid.
|
||||
/// </summary>
|
||||
private const int ERROR_INVALID_REPARSE_DATA = 4392;
|
||||
|
||||
/// <summary>
|
||||
/// The tag present in the reparse point buffer is invalid.
|
||||
/// </summary>
|
||||
private const int ERROR_REPARSE_TAG_INVALID = 4393;
|
||||
|
||||
/// <summary>
|
||||
/// There is a mismatch between the tag specified in the request and the tag present in the reparse point.
|
||||
/// </summary>
|
||||
private const int ERROR_REPARSE_TAG_MISMATCH = 4394;
|
||||
|
||||
/// <summary>
|
||||
/// Command to set the reparse point data block.
|
||||
/// </summary>
|
||||
private const int FSCTL_SET_REPARSE_POINT = 0x000900A4;
|
||||
|
||||
/// <summary>
|
||||
/// Command to get the reparse point data block.
|
||||
/// </summary>
|
||||
private const int FSCTL_GET_REPARSE_POINT = 0x000900A8;
|
||||
|
||||
/// <summary>
|
||||
/// Command to delete the reparse point data base.
|
||||
/// </summary>
|
||||
private const int FSCTL_DELETE_REPARSE_POINT = 0x000900AC;
|
||||
|
||||
/// <summary>
|
||||
/// Reparse point tag used to identify mount points and junction points.
|
||||
/// </summary>
|
||||
private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
|
||||
|
||||
/// <summary>
|
||||
/// This prefix indicates to NTFS that the path is to be treated as a non-interpreted
|
||||
/// path in the virtual file system.
|
||||
/// </summary>
|
||||
private const string NonInterpretedPathPrefix = @"\??\";
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
|
||||
IntPtr InBuffer, int nInBufferSize,
|
||||
IntPtr OutBuffer, int nOutBufferSize,
|
||||
out int pBytesReturned, IntPtr lpOverlapped);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr CreateFile(
|
||||
string lpFileName,
|
||||
EFileAccess dwDesiredAccess,
|
||||
EFileShare dwShareMode,
|
||||
IntPtr lpSecurityAttributes,
|
||||
ECreationDisposition dwCreationDisposition,
|
||||
EFileAttributes dwFlagsAndAttributes,
|
||||
IntPtr hTemplateFile);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a junction point from the specified directory to the specified target directory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only works on NTFS.
|
||||
/// </remarks>
|
||||
/// <param name="junctionPoint">The junction point path</param>
|
||||
/// <param name="targetDir">The target directory</param>
|
||||
/// <param name="overwrite">If true overwrites an existing reparse point or empty directory</param>
|
||||
/// <exception cref="IOException">
|
||||
/// Thrown when the junction point could not be created or when
|
||||
/// an existing directory was found and <paramref name="overwrite" /> if false
|
||||
/// </exception>
|
||||
public static void Create(string junctionPoint, string targetDir, bool overwrite)
|
||||
{
|
||||
targetDir = Path.GetFullPath(targetDir);
|
||||
|
||||
if (!Directory.Exists(targetDir))
|
||||
throw new IOException("Target path does not exist or is not a directory.");
|
||||
|
||||
if (Directory.Exists(junctionPoint)) {
|
||||
if (!overwrite)
|
||||
throw new IOException("Directory already exists and overwrite parameter is false.");
|
||||
} else {
|
||||
Directory.CreateDirectory(junctionPoint);
|
||||
}
|
||||
|
||||
using (var handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite)) {
|
||||
var targetDirBytes = Encoding.Unicode.GetBytes(NonInterpretedPathPrefix + Path.GetFullPath(targetDir));
|
||||
|
||||
var reparseDataBuffer =
|
||||
new REPARSE_DATA_BUFFER {
|
||||
ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
|
||||
ReparseDataLength = (ushort) (targetDirBytes.Length + 12),
|
||||
SubstituteNameOffset = 0,
|
||||
SubstituteNameLength = (ushort) targetDirBytes.Length,
|
||||
PrintNameOffset = (ushort) (targetDirBytes.Length + 2),
|
||||
PrintNameLength = 0,
|
||||
PathBuffer = new byte[0x3ff0]
|
||||
};
|
||||
|
||||
Array.Copy(targetDirBytes, reparseDataBuffer.PathBuffer, targetDirBytes.Length);
|
||||
|
||||
var inBufferSize = Marshal.SizeOf(reparseDataBuffer);
|
||||
var inBuffer = Marshal.AllocHGlobal(inBufferSize);
|
||||
|
||||
try {
|
||||
Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);
|
||||
|
||||
int bytesReturned;
|
||||
var result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_SET_REPARSE_POINT,
|
||||
inBuffer, targetDirBytes.Length + 20, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);
|
||||
|
||||
if (!result)
|
||||
ThrowLastWin32Error("Unable to create junction point.");
|
||||
} finally {
|
||||
Marshal.FreeHGlobal(inBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a junction point at the specified source directory along with the directory itself.
|
||||
/// Does nothing if the junction point does not exist.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only works on NTFS.
|
||||
/// </remarks>
|
||||
/// <param name="junctionPoint">The junction point path</param>
|
||||
public static void Delete(string junctionPoint)
|
||||
{
|
||||
if (!Directory.Exists(junctionPoint)) {
|
||||
if (File.Exists(junctionPoint))
|
||||
throw new IOException("Path is not a junction point.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
using (var handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericWrite)) {
|
||||
var reparseDataBuffer = new REPARSE_DATA_BUFFER {
|
||||
ReparseTag = IO_REPARSE_TAG_MOUNT_POINT,
|
||||
ReparseDataLength = 0,
|
||||
PathBuffer = new byte[0x3ff0]
|
||||
};
|
||||
|
||||
|
||||
var inBufferSize = Marshal.SizeOf(reparseDataBuffer);
|
||||
var inBuffer = Marshal.AllocHGlobal(inBufferSize);
|
||||
try {
|
||||
Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false);
|
||||
|
||||
int bytesReturned;
|
||||
var result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_DELETE_REPARSE_POINT,
|
||||
inBuffer, 8, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero);
|
||||
|
||||
if (!result)
|
||||
ThrowLastWin32Error("Unable to delete junction point.");
|
||||
} finally {
|
||||
Marshal.FreeHGlobal(inBuffer);
|
||||
}
|
||||
|
||||
try {
|
||||
Directory.Delete(junctionPoint);
|
||||
} catch (IOException ex) {
|
||||
throw new IOException("Unable to delete junction point.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified path exists and refers to a junction point.
|
||||
/// </summary>
|
||||
/// <param name="path">The junction point path</param>
|
||||
/// <returns>True if the specified path represents a junction point</returns>
|
||||
/// <exception cref="IOException">
|
||||
/// Thrown if the specified path is invalid
|
||||
/// or some other error occurs
|
||||
/// </exception>
|
||||
public static bool Exists(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
return false;
|
||||
|
||||
using (var handle = OpenReparsePoint(path, EFileAccess.GenericRead)) {
|
||||
var target = InternalGetTarget(handle);
|
||||
return target != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target of the specified junction point.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only works on NTFS.
|
||||
/// </remarks>
|
||||
/// <param name="junctionPoint">The junction point path</param>
|
||||
/// <returns>The target of the junction point</returns>
|
||||
/// <exception cref="IOException">
|
||||
/// Thrown when the specified path does not
|
||||
/// exist, is invalid, is not a junction point, or some other error occurs
|
||||
/// </exception>
|
||||
public static string GetTarget(string junctionPoint)
|
||||
{
|
||||
using (var handle = OpenReparsePoint(junctionPoint, EFileAccess.GenericRead)) {
|
||||
var target = InternalGetTarget(handle);
|
||||
if (target == null)
|
||||
throw new IOException("Path is not a junction point.");
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
private static string InternalGetTarget(SafeFileHandle handle)
|
||||
{
|
||||
var outBufferSize = Marshal.SizeOf(typeof(REPARSE_DATA_BUFFER));
|
||||
var outBuffer = Marshal.AllocHGlobal(outBufferSize);
|
||||
|
||||
try {
|
||||
int bytesReturned;
|
||||
var result = DeviceIoControl(handle.DangerousGetHandle(), FSCTL_GET_REPARSE_POINT,
|
||||
IntPtr.Zero, 0, outBuffer, outBufferSize, out bytesReturned, IntPtr.Zero);
|
||||
|
||||
if (!result) {
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
if (error == ERROR_NOT_A_REPARSE_POINT)
|
||||
return null;
|
||||
|
||||
ThrowLastWin32Error("Unable to get information about junction point.");
|
||||
}
|
||||
|
||||
var reparseDataBuffer = (REPARSE_DATA_BUFFER)
|
||||
Marshal.PtrToStructure(outBuffer, typeof(REPARSE_DATA_BUFFER));
|
||||
|
||||
if (reparseDataBuffer.ReparseTag != IO_REPARSE_TAG_MOUNT_POINT)
|
||||
return null;
|
||||
|
||||
var targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer,
|
||||
reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength);
|
||||
|
||||
if (targetDir.StartsWith(NonInterpretedPathPrefix))
|
||||
targetDir = targetDir.Substring(NonInterpretedPathPrefix.Length);
|
||||
|
||||
return targetDir;
|
||||
} finally {
|
||||
Marshal.FreeHGlobal(outBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static SafeFileHandle OpenReparsePoint(string reparsePoint, EFileAccess accessMode)
|
||||
{
|
||||
var reparsePointHandle = new SafeFileHandle(CreateFile(reparsePoint, accessMode,
|
||||
EFileShare.Read | EFileShare.Write | EFileShare.Delete,
|
||||
IntPtr.Zero, ECreationDisposition.OpenExisting,
|
||||
EFileAttributes.BackupSemantics | EFileAttributes.OpenReparsePoint, IntPtr.Zero), true);
|
||||
|
||||
if (Marshal.GetLastWin32Error() != 0)
|
||||
ThrowLastWin32Error("Unable to open reparse point.");
|
||||
|
||||
return reparsePointHandle;
|
||||
}
|
||||
|
||||
private static void ThrowLastWin32Error(string message)
|
||||
{
|
||||
throw new IOException(message, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum EFileAccess : uint
|
||||
{
|
||||
GenericRead = 0x80000000,
|
||||
GenericWrite = 0x40000000,
|
||||
GenericExecute = 0x20000000,
|
||||
GenericAll = 0x10000000
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum EFileShare : uint
|
||||
{
|
||||
None = 0x00000000,
|
||||
Read = 0x00000001,
|
||||
Write = 0x00000002,
|
||||
Delete = 0x00000004
|
||||
}
|
||||
|
||||
private enum ECreationDisposition : uint
|
||||
{
|
||||
New = 1,
|
||||
CreateAlways = 2,
|
||||
OpenExisting = 3,
|
||||
OpenAlways = 4,
|
||||
TruncateExisting = 5
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum EFileAttributes : uint
|
||||
{
|
||||
Readonly = 0x00000001,
|
||||
Hidden = 0x00000002,
|
||||
System = 0x00000004,
|
||||
Directory = 0x00000010,
|
||||
Archive = 0x00000020,
|
||||
Device = 0x00000040,
|
||||
Normal = 0x00000080,
|
||||
Temporary = 0x00000100,
|
||||
SparseFile = 0x00000200,
|
||||
ReparsePoint = 0x00000400,
|
||||
Compressed = 0x00000800,
|
||||
Offline = 0x00001000,
|
||||
NotContentIndexed = 0x00002000,
|
||||
Encrypted = 0x00004000,
|
||||
Write_Through = 0x80000000,
|
||||
Overlapped = 0x40000000,
|
||||
NoBuffering = 0x20000000,
|
||||
RandomAccess = 0x10000000,
|
||||
SequentialScan = 0x08000000,
|
||||
DeleteOnClose = 0x04000000,
|
||||
BackupSemantics = 0x02000000,
|
||||
PosixSemantics = 0x01000000,
|
||||
OpenReparsePoint = 0x00200000,
|
||||
OpenNoRecall = 0x00100000,
|
||||
FirstPipeInstance = 0x00080000
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct REPARSE_DATA_BUFFER
|
||||
{
|
||||
/// <summary>
|
||||
/// Reparse point tag. Must be a Microsoft reparse point tag.
|
||||
/// </summary>
|
||||
public uint ReparseTag;
|
||||
|
||||
/// <summary>
|
||||
/// Size, in bytes, of the data after the Reserved member. This can be calculated by:
|
||||
/// (4 * sizeof(ushort)) + SubstituteNameLength + PrintNameLength +
|
||||
/// (namesAreNullTerminated ? 2 * sizeof(char) : 0);
|
||||
/// </summary>
|
||||
public ushort ReparseDataLength;
|
||||
|
||||
/// <summary>
|
||||
/// Reserved; do not use.
|
||||
/// </summary>
|
||||
public ushort Reserved;
|
||||
|
||||
/// <summary>
|
||||
/// Offset, in bytes, of the substitute name string in the PathBuffer array.
|
||||
/// </summary>
|
||||
public ushort SubstituteNameOffset;
|
||||
|
||||
/// <summary>
|
||||
/// Length, in bytes, of the substitute name string. If this string is null-terminated,
|
||||
/// SubstituteNameLength does not include space for the null character.
|
||||
/// </summary>
|
||||
public ushort SubstituteNameLength;
|
||||
|
||||
/// <summary>
|
||||
/// Offset, in bytes, of the print name string in the PathBuffer array.
|
||||
/// </summary>
|
||||
public ushort PrintNameOffset;
|
||||
|
||||
/// <summary>
|
||||
/// Length, in bytes, of the print name string. If this string is null-terminated,
|
||||
/// PrintNameLength does not include space for the null character.
|
||||
/// </summary>
|
||||
public ushort PrintNameLength;
|
||||
|
||||
/// <summary>
|
||||
/// A buffer containing the unicode-encoded path string. The path string contains
|
||||
/// the substitute name string and print name string.
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3FF0)] public byte[] PathBuffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
54
src/Squirrel/LoggerExtensions.cs
Normal file
54
src/Squirrel/LoggerExtensions.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
public static class LoggerExtensions
|
||||
{
|
||||
public static void Warn(this ILogger logger, string message)
|
||||
{
|
||||
logger.LogWarning(message);
|
||||
}
|
||||
|
||||
public static void Warn(this ILogger logger, Exception ex, string message)
|
||||
{
|
||||
logger.LogWarning(ex, message);
|
||||
}
|
||||
|
||||
public static void Warn(this ILogger logger, Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, ex.Message);
|
||||
}
|
||||
|
||||
public static void Info(this ILogger logger, string message)
|
||||
{
|
||||
logger.LogInformation(message);
|
||||
}
|
||||
|
||||
public static void Error(this ILogger logger, string message)
|
||||
{
|
||||
logger.LogError(message);
|
||||
}
|
||||
|
||||
public static void Error(this ILogger logger, Exception ex, string message)
|
||||
{
|
||||
logger.LogError(ex, message);
|
||||
}
|
||||
|
||||
public static void Error(this ILogger logger, Exception ex)
|
||||
{
|
||||
logger.LogError(ex, ex.Message);
|
||||
}
|
||||
|
||||
public static void Debug(this ILogger logger, string message)
|
||||
{
|
||||
logger.LogDebug(message);
|
||||
}
|
||||
|
||||
public static void Trace(this ILogger logger, string message)
|
||||
{
|
||||
logger.LogTrace(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Squirrel/NuGet/ZipExtensions.cs
Normal file
15
src/Squirrel/NuGet/ZipExtensions.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
using System;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Squirrel.NuGet
|
||||
{
|
||||
public static class ZipExtensions
|
||||
{
|
||||
public static bool IsDirectory(this ZipArchiveEntry entry)
|
||||
{
|
||||
return entry.FullName.EndsWith("/") || entry.FullName.EndsWith("\\") || String.IsNullOrEmpty(entry.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel.SimpleSplat;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Squirrel.NuGet
|
||||
{
|
||||
@@ -90,13 +90,13 @@ namespace Squirrel.NuGet
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static Task ExtractZipReleaseForInstall(string zipFilePath, string outFolder, string rootPackageFolder, Action<int> progress)
|
||||
public static Task ExtractZipReleaseForInstall(ILogger logger, string zipFilePath, string outFolder, string rootPackageFolder, Action<int> progress)
|
||||
{
|
||||
if (SquirrelRuntimeInfo.IsWindows)
|
||||
return ExtractZipReleaseForInstallWindows(zipFilePath, outFolder, rootPackageFolder, progress);
|
||||
return ExtractZipReleaseForInstallWindows(logger, zipFilePath, outFolder, rootPackageFolder, progress);
|
||||
|
||||
if (SquirrelRuntimeInfo.IsOSX)
|
||||
return ExtractZipReleaseForInstallOSX(zipFilePath, outFolder, progress);
|
||||
return ExtractZipReleaseForInstallOSX(logger, zipFilePath, outFolder, progress);
|
||||
|
||||
throw new NotSupportedException("Platform not supported.");
|
||||
}
|
||||
@@ -105,7 +105,7 @@ namespace Squirrel.NuGet
|
||||
new Regex(@"lib[\\\/][^\\\/]*[\\\/]", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
[SupportedOSPlatform("macos")]
|
||||
public static Task ExtractZipReleaseForInstallOSX(string zipFilePath, string outFinalFolder, Action<int> progress)
|
||||
public static Task ExtractZipReleaseForInstallOSX(ILogger logger, string zipFilePath, string outFinalFolder, Action<int> progress)
|
||||
{
|
||||
if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist");
|
||||
progress ??= ((_) => { });
|
||||
@@ -161,7 +161,7 @@ namespace Squirrel.NuGet
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static Task ExtractZipReleaseForInstallWindows(string zipFilePath, string outFinalFolder, string rootPackageFolder, Action<int> progress)
|
||||
public static Task ExtractZipReleaseForInstallWindows(ILogger logger, string zipFilePath, string outFinalFolder, string rootPackageFolder, Action<int> progress)
|
||||
{
|
||||
if (!File.Exists(zipFilePath)) throw new ArgumentException("zipFilePath must exist");
|
||||
progress ??= ((_) => { });
|
||||
@@ -198,13 +198,15 @@ namespace Squirrel.NuGet
|
||||
var failureIsOkay = false;
|
||||
if (!entry.IsDirectory() && decoded.Contains("_ExecutionStub.exe")) {
|
||||
// NB: On upgrade, many of these stubs will be in-use, nbd tho.
|
||||
failureIsOkay = true;
|
||||
//failureIsOkay = true;
|
||||
|
||||
fullTargetFile = Path.Combine(
|
||||
rootPackageFolder,
|
||||
Path.GetFileName(decoded).Replace("_ExecutionStub.exe", ".exe"));
|
||||
//fullTargetFile = Path.Combine(
|
||||
// rootPackageFolder,
|
||||
// Path.GetFileName(decoded).Replace("_ExecutionStub.exe", ".exe"));
|
||||
|
||||
LogHost.Default.Info("Rigging execution stub for {0} to {1}", decoded, fullTargetFile);
|
||||
//logger.Info($"Rigging execution stub for {decoded} to {fullTargetFile}");
|
||||
logger.Info($"Skipping obsolete stub {decoded}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Utility.PathPartEquals(parts.Last(), "app.ico")) {
|
||||
@@ -222,7 +224,7 @@ namespace Squirrel.NuGet
|
||||
});
|
||||
} catch (Exception e) {
|
||||
if (!failureIsOkay) throw;
|
||||
LogHost.Default.WarnException("Can't write execution stub, probably in use", e);
|
||||
logger.Warn(e, "Can't write execution stub, probably in use");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: InternalsVisibleTo("Squirrel.Tests, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Squirrel, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("SquirrelMac, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("SquirrelCli, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine.OSX, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Squirrel.CommandLine.Windows, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Update, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("UpdateMac, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Update.Windows, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("Update.OSX, PublicKey=" + SNK.SHA1)]
|
||||
[assembly: InternalsVisibleTo("csq, PublicKey=" + SNK.SHA1)]
|
||||
|
||||
internal static class SNK
|
||||
{
|
||||
public const string SHA1 = "002400000480000094000000060200000024000052534131000400000100010061b199572531d267773d7783a077bc020aacb34a10d8c11407505a4a814284d4c953df3229ccf8f63d1a410a3395b7266e5e5cba8f1c0bc9ee10fc7ddafdae297431e2eef82eccd2ac8957bfc9119063f4a965d6ae3ccf53e1f4d8e9ce894a79ea1f681eb2067745c5253f6747cbc51eec640dd2edb4a67339b44f093e3ec5b0";
|
||||
}
|
||||
@@ -10,7 +10,6 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using NuGet.Versioning;
|
||||
using Squirrel.NuGet;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace Squirrel
|
||||
{
|
||||
@@ -79,7 +78,7 @@ namespace Squirrel
|
||||
|
||||
/// <inheritdoc cref="IReleaseEntry" />
|
||||
[DataContract]
|
||||
public class ReleaseEntry : IEnableLogger, IReleaseEntry
|
||||
public class ReleaseEntry : IReleaseEntry
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DataMember] public string SHA1 { get; protected set; }
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace Squirrel.Sources
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple abstractable file downloader
|
||||
/// </summary>
|
||||
public interface IFileDownloader : IEnableLogger
|
||||
public interface IFileDownloader
|
||||
{
|
||||
/// <summary>
|
||||
/// Downloads a remote file to the specified local path
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace Squirrel.Sources
|
||||
{
|
||||
@@ -9,7 +8,7 @@ namespace Squirrel.Sources
|
||||
/// An implementation may copy a file from a local repository, download from a web address,
|
||||
/// or even use third party services and parse proprietary data to produce a package feed.
|
||||
/// </summary>
|
||||
public interface IUpdateSource : IEnableLogger
|
||||
public interface IUpdateSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve the list of available remote releases from the package source. These releases
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel.SimpleSplat;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Squirrel.Sources
|
||||
{
|
||||
@@ -13,13 +13,16 @@ namespace Squirrel.Sources
|
||||
/// </summary>
|
||||
public class SimpleFileSource : IUpdateSource
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary> The local directory containing packages to update to. </summary>
|
||||
public virtual DirectoryInfo BaseDirectory { get; }
|
||||
|
||||
/// <inheritdoc cref="SimpleFileSource" />
|
||||
/// <param name="baseDirectory">The directory where to search for packages.</param>
|
||||
public SimpleFileSource(DirectoryInfo baseDirectory)
|
||||
public SimpleFileSource(ILogger logger, DirectoryInfo baseDirectory)
|
||||
{
|
||||
_logger = logger;
|
||||
BaseDirectory = baseDirectory;
|
||||
}
|
||||
|
||||
@@ -30,7 +33,7 @@ namespace Squirrel.Sources
|
||||
throw new Exception($"The local update directory '{BaseDirectory.FullName}' does not exist.");
|
||||
|
||||
var releasesPath = Path.Combine(BaseDirectory.FullName, "RELEASES");
|
||||
this.Log().Info($"Reading RELEASES from '{releasesPath}'");
|
||||
_logger.Info($"Reading RELEASES from '{releasesPath}'");
|
||||
var fi = new FileInfo(releasesPath);
|
||||
|
||||
if (fi.Exists) {
|
||||
@@ -39,7 +42,7 @@ namespace Squirrel.Sources
|
||||
} else {
|
||||
var packages = BaseDirectory.EnumerateFiles("*.nupkg");
|
||||
if (packages.Any()) {
|
||||
this.Log().Warn($"The file '{releasesPath}' does not exist but directory contains packages. " +
|
||||
_logger.Warn($"The file '{releasesPath}' does not exist but directory contains packages. " +
|
||||
$"This is not valid but attempting to proceed anyway by writing new file.");
|
||||
return Task.FromResult(ReleaseEntry.BuildReleasesFile(BaseDirectory.FullName).ToArray());
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel.SimpleSplat;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Squirrel.Sources
|
||||
{
|
||||
@@ -13,6 +13,8 @@ namespace Squirrel.Sources
|
||||
/// </summary>
|
||||
public class SimpleWebSource : IUpdateSource
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary> The URL of the server hosting packages to update to. </summary>
|
||||
public virtual Uri BaseUri { get; }
|
||||
|
||||
@@ -20,13 +22,14 @@ namespace Squirrel.Sources
|
||||
public virtual IFileDownloader Downloader { get; }
|
||||
|
||||
/// <inheritdoc cref="SimpleWebSource" />
|
||||
public SimpleWebSource(string baseUrl, IFileDownloader downloader = null)
|
||||
: this(new Uri(baseUrl), downloader)
|
||||
public SimpleWebSource(ILogger logger, string baseUrl, IFileDownloader downloader = null)
|
||||
: this(logger, new Uri(baseUrl), downloader)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="SimpleWebSource" />
|
||||
public SimpleWebSource(Uri baseUri, IFileDownloader downloader = null)
|
||||
public SimpleWebSource(ILogger logger, Uri baseUri, IFileDownloader downloader = null)
|
||||
{
|
||||
_logger = logger;
|
||||
BaseUri = baseUri;
|
||||
Downloader = downloader ?? Utility.CreateDefaultDownloader();
|
||||
}
|
||||
@@ -54,7 +57,7 @@ namespace Squirrel.Sources
|
||||
|
||||
var uriAndQuery = Utility.AddQueryParamsToUri(uri, args);
|
||||
|
||||
this.Log().Info($"Downloading RELEASES from '{uriAndQuery}'.");
|
||||
_logger.Info($"Downloading RELEASES from '{uriAndQuery}'.");
|
||||
|
||||
var bytes = await Downloader.DownloadBytes(uriAndQuery.ToString()).ConfigureAwait(false);
|
||||
var txt = Utility.RemoveByteOrderMarkerIfPresent(bytes);
|
||||
@@ -83,7 +86,7 @@ namespace Squirrel.Sources
|
||||
? new Uri(sourceBaseUri, releaseUri).ToString()
|
||||
: Utility.AppendPathToUri(sourceBaseUri, releaseUri).ToString();
|
||||
|
||||
this.Log().Info($"Downloading '{releaseEntry.Filename}' from '{source}'.");
|
||||
_logger.Info($"Downloading '{releaseEntry.Filename}' from '{source}'.");
|
||||
return Downloader.DownloadFile(source, localFile, progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Squirrel.SimpleSplat;
|
||||
|
||||
namespace Squirrel.Sources
|
||||
{
|
||||
/// This class is obsolete. Use <see cref="HttpClientFileDownloader"/> instead.
|
||||
[Obsolete("Use HttpClientFileDownloader")]
|
||||
public class WebClientFileDownloader : IFileDownloader
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public virtual async Task DownloadFile(string url, string targetFile, Action<int> progress, string authorization, string accept)
|
||||
{
|
||||
using (var wc = CreateWebClient(authorization, accept)) {
|
||||
var failedUrl = default(string);
|
||||
|
||||
var lastSignalled = DateTime.MinValue;
|
||||
wc.DownloadProgressChanged += (sender, args) => {
|
||||
var now = DateTime.Now;
|
||||
|
||||
if (now - lastSignalled > TimeSpan.FromMilliseconds(500)) {
|
||||
lastSignalled = now;
|
||||
progress(args.ProgressPercentage);
|
||||
}
|
||||
};
|
||||
|
||||
retry:
|
||||
try {
|
||||
this.Log().Info("Downloading file: " + (failedUrl ?? url));
|
||||
|
||||
await this.WarnIfThrows(
|
||||
async () => {
|
||||
await wc.DownloadFileTaskAsync(failedUrl ?? url, targetFile).ConfigureAwait(false);
|
||||
progress(100);
|
||||
},
|
||||
"Failed downloading URL: " + (failedUrl ?? url)).ConfigureAwait(false);
|
||||
} catch (Exception) {
|
||||
// NB: Some super brain-dead services are case-sensitive yet
|
||||
// corrupt case on upload. I can't even.
|
||||
if (failedUrl != null) throw;
|
||||
|
||||
failedUrl = url.ToLower();
|
||||
progress(0);
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<byte[]> DownloadBytes(string url, string authorization, string accept)
|
||||
{
|
||||
using (var wc = CreateWebClient(authorization, accept)) {
|
||||
var failedUrl = default(string);
|
||||
|
||||
retry:
|
||||
try {
|
||||
this.Log().Info("Downloading url: " + (failedUrl ?? url));
|
||||
|
||||
return await this.WarnIfThrows(() => wc.DownloadDataTaskAsync(failedUrl ?? url),
|
||||
"Failed to download url: " + (failedUrl ?? url)).ConfigureAwait(false);
|
||||
} catch (Exception) {
|
||||
// NB: Some super brain-dead services are case-sensitive yet
|
||||
// corrupt case on upload. I can't even.
|
||||
if (failedUrl != null) throw;
|
||||
|
||||
failedUrl = url.ToLower();
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<string> DownloadString(string url, string authorization, string accept)
|
||||
{
|
||||
using (var wc = CreateWebClient(authorization, accept)) {
|
||||
var failedUrl = default(string);
|
||||
|
||||
retry:
|
||||
try {
|
||||
this.Log().Info("Downloading url: " + (failedUrl ?? url));
|
||||
|
||||
return await this.WarnIfThrows(() => wc.DownloadStringTaskAsync(failedUrl ?? url),
|
||||
"Failed to download url: " + (failedUrl ?? url)).ConfigureAwait(false);
|
||||
} catch (Exception) {
|
||||
// NB: Some super brain-dead services are case-sensitive yet
|
||||
// corrupt case on upload. I can't even.
|
||||
if (failedUrl != null) throw;
|
||||
|
||||
failedUrl = url.ToLower();
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and returns a new WebClient for every requst
|
||||
/// </summary>
|
||||
protected virtual WebClient CreateWebClient(string authorization, string accept)
|
||||
{
|
||||
var ret = new WebClient();
|
||||
var wp = WebRequest.DefaultWebProxy;
|
||||
if (wp != null) {
|
||||
wp.Credentials = CredentialCache.DefaultCredentials;
|
||||
ret.Proxy = wp;
|
||||
}
|
||||
|
||||
if (authorization != null)
|
||||
ret.Headers.Add("Authorization", authorization);
|
||||
|
||||
if (accept != null)
|
||||
ret.Headers.Add("Accept", accept);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user