Improved logic.
This commit is contained in:
@@ -0,0 +1,184 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using Renci.SshNet;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace aeqw89.tools.Publish;
|
||||||
|
|
||||||
|
sealed record DestinationContext(
|
||||||
|
ProgressTask Task,
|
||||||
|
ProgressContext ProgressContext,
|
||||||
|
MemoryStream Reader,
|
||||||
|
FileInfo PackageFile,
|
||||||
|
string Name,
|
||||||
|
long BufferSize,
|
||||||
|
long PackageSize
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IDestination {
|
||||||
|
Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed record LocalDestination(DestinationContext Context) : IDestination {
|
||||||
|
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
|
||||||
|
// Path = "$USER/local-%ctx.name/%pkg.filename"
|
||||||
|
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Context.Name, Context.PackageFile.Name);
|
||||||
|
try {
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!); // create the directory
|
||||||
|
await using var writer = File.OpenWrite(path);
|
||||||
|
var buffer = new byte[Context.BufferSize];
|
||||||
|
int read;
|
||||||
|
do {
|
||||||
|
read = await Context.Reader.ReadAsync(buffer, ct);
|
||||||
|
writer.Write(buffer, 0, read);
|
||||||
|
Context.Task.Increment(read);
|
||||||
|
} while (read > 0);
|
||||||
|
return Success.AsResult();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new ReadableError(string.Format(Exceptions.generic_error, e), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed record CloudDestiantion(DestinationContext Context) : IDestination {
|
||||||
|
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
|
||||||
|
var connectionTask = Context.ProgressContext.AddTaskBefore($"Preparing cloud-{Context.Name}",
|
||||||
|
new ProgressTaskSettings() {
|
||||||
|
MaxValue = 100
|
||||||
|
}, Context.Task);
|
||||||
|
|
||||||
|
if (!SshHosts.TryGetHost(Context.Name, out var host)) {
|
||||||
|
return new ReadableError(string.Format(Exceptions.cloud_host_not_found.EscapeMarkup(), Context.Name), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var connectionInfo = SshHosts.GetConnection(Context.Name);
|
||||||
|
using var sshClient = new SshClient(connectionInfo);
|
||||||
|
|
||||||
|
if (!sshClient.IsConnected)
|
||||||
|
await sshClient.ConnectAsync(ct);
|
||||||
|
|
||||||
|
connectionTask.Increment(33); // ? One-Third of the way done (connection success).
|
||||||
|
|
||||||
|
var winC = sshClient.RunCommand("cmd /c ver");
|
||||||
|
var othC = sshClient.RunCommand("uname -s");
|
||||||
|
|
||||||
|
var os = (winC.ExitStatus, othC.ExitStatus) switch {
|
||||||
|
(0, _) => "windows",
|
||||||
|
(_, 0) => "linux",
|
||||||
|
_ => "unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
string remoteDirectory;
|
||||||
|
string packageFileDirectory;
|
||||||
|
|
||||||
|
if (os == "windows") {
|
||||||
|
var userDirC = sshClient.RunCommand("cmd /c echo %USERPROFILE%");
|
||||||
|
if (userDirC.ExitStatus != 0) { // * missing env variable case (could not work out where to place packages in remote).
|
||||||
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, "n/a", Context.PackageFile.Name, os,
|
||||||
|
userDirC.Result), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userDir = userDirC.Result.Trim();
|
||||||
|
remoteDirectory = RemotePath.Combine(RemoteOs.Windows, userDir, "dotnet-packages");
|
||||||
|
packageFileDirectory = RemotePath.Combine(RemoteOs.Windows, remoteDirectory, Context.PackageFile.Name);
|
||||||
|
|
||||||
|
var mkdirC = sshClient.RunCommand($"cmd /c if not exist \"{remoteDirectory}\" mkdir \"{remoteDirectory}\"");
|
||||||
|
if (mkdirC.ExitStatus != 0) { // * MKDir error case (make directory command failed on remote).
|
||||||
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, remoteDirectory, Context.PackageFile.Name, os,
|
||||||
|
mkdirC.Result), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (os == "linux") {
|
||||||
|
var homeDirC = sshClient.RunCommand("printf %s \"$HOME\"");
|
||||||
|
if (homeDirC.ExitStatus != 0) { // * missing env variable case 2 (could not work out where to place packages in remote).
|
||||||
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, "n/a", Context.PackageFile.Name, os,
|
||||||
|
homeDirC.Result), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var homeDir = homeDirC.Result.Trim(); // no CRLF on unix, but Trim() is safest
|
||||||
|
remoteDirectory = RemotePath.Combine(RemoteOs.Unix, homeDir, ".dotnet-packages");
|
||||||
|
packageFileDirectory = RemotePath.Combine(RemoteOs.Unix, remoteDirectory,
|
||||||
|
Context.PackageFile.Name);
|
||||||
|
|
||||||
|
// Use -p and single quotes to handle spaces safely
|
||||||
|
var mkdirC = sshClient.RunCommand($"mkdir -p '{remoteDirectory}'");
|
||||||
|
if (mkdirC.ExitStatus != 0) { // * MKDir error case (make directory command failed on remote).
|
||||||
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, remoteDirectory, Context.PackageFile.Name,
|
||||||
|
os, mkdirC.Result), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, "n/a", Context.PackageFile.Name, os,
|
||||||
|
"Unsupported OS"), false); // * MacOS failure path (or other unrecognizable oses).
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionTask.Increment(33); // ? Two-Thirds of the way done (directory located and ready).
|
||||||
|
|
||||||
|
sshClient.Disconnect();
|
||||||
|
|
||||||
|
using var client = new SftpClient(connectionInfo);
|
||||||
|
if (!client.IsConnected)
|
||||||
|
await client.ConnectAsync(ct);
|
||||||
|
connectionTask.Increment(33); // ? Connection task complete (now proceeding to upload task).
|
||||||
|
connectionTask.StopTask();
|
||||||
|
|
||||||
|
await using var writer = client.OpenWrite(packageFileDirectory);
|
||||||
|
byte[] buffer = new byte[Context.BufferSize];
|
||||||
|
int read;
|
||||||
|
do {
|
||||||
|
read = await Context.Reader.ReadAsync(buffer, ct);
|
||||||
|
writer.Write(buffer, 0, read);
|
||||||
|
Context.Task.Increment(read);
|
||||||
|
} while (read > 0);
|
||||||
|
|
||||||
|
return Success.AsResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed record GithubDestination(DestinationContext Context, bool Verbose = false) : IDestination {
|
||||||
|
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
|
||||||
|
var p = Process.Start(new ProcessStartInfo() {
|
||||||
|
FileName = "dotnet",
|
||||||
|
Arguments = $"nuget push \"{Context.PackageFile.FullName}\" --source github",
|
||||||
|
WorkingDirectory = Environment.CurrentDirectory,
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
StringBuilder errorLines = new();
|
||||||
|
p?.ErrorDataReceived += (sender, eventArgs) => {
|
||||||
|
cts.Cancel();
|
||||||
|
if (Verbose && eventArgs.Data != null)
|
||||||
|
AnsiConsole.WriteLine(eventArgs.Data);
|
||||||
|
errorLines.Append(eventArgs.Data);
|
||||||
|
};
|
||||||
|
p?.OutputDataReceived += (sender, eventArgs) => {
|
||||||
|
if (eventArgs.Data?.ToLower().Contains("press any key") == true)
|
||||||
|
cts.Cancel();
|
||||||
|
if (Verbose && eventArgs.Data != null)
|
||||||
|
AnsiConsole.WriteLine(eventArgs.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
p?.BeginOutputReadLine();
|
||||||
|
p?.BeginErrorReadLine();
|
||||||
|
|
||||||
|
Context.Task.Increment(Context.PackageSize / 2);
|
||||||
|
if (p != null)
|
||||||
|
try {
|
||||||
|
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException) {
|
||||||
|
p?.Kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p?.ExitCode != 0) {
|
||||||
|
Context.Task.StopTask();
|
||||||
|
return new ReadableError(errorLines.ToString().EscapeMarkup() + "\n" + string.Format(Exceptions.dotnet_nuget_push_failure, p?.ExitCode ?? -1), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Context.Task.Increment(Context.BufferSize / 2);
|
||||||
|
return Success.AsResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace aeqw89.tools.Publish;
|
||||||
|
|
||||||
|
record Success {
|
||||||
|
public static Ok<Success> AsResult() => new Ok<Success>(new Success());
|
||||||
|
}
|
||||||
|
record Ok<T>(T Value);
|
||||||
|
record ReadableError(string Reason, bool ShowHelp = true, bool RestoreContext = true);
|
||||||
|
union Result<T, E>(Ok<T>, E);
|
||||||
|
|
||||||
|
internal static class ResultExtensions {
|
||||||
|
public static T Unwrap<T>(this Result<T, ReadableError> result, Program.RunContext? rctx = null) {
|
||||||
|
switch (result) {
|
||||||
|
case Ok<T> r:
|
||||||
|
return r.Value;
|
||||||
|
case ReadableError error:
|
||||||
|
Program.ShowError(error.Reason);
|
||||||
|
if (error.ShowHelp) Program.ShowHelp();
|
||||||
|
if (error.RestoreContext) rctx?.Restore();
|
||||||
|
Environment.Exit(1);
|
||||||
|
throw new UnreachableException();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnreachableException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<T> Unwrap<T>(this Task<Result<T, ReadableError>> result, Program.RunContext? rctx = null) {
|
||||||
|
return (await result).Unwrap(rctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ObjectExtensions {
|
||||||
|
public static Ok<T> Ok<T>(this T any) {
|
||||||
|
return new Ok<T>(any);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace aeqw89.tools.Publish;
|
||||||
|
|
||||||
|
internal static class Shell {
|
||||||
|
internal record ShellResult(string Output, string Error, int ExitCode);
|
||||||
|
internal record PackageInfo(FileInfo FileInfo) {
|
||||||
|
byte[]? _inMemory = null;
|
||||||
|
public long Size() => FileInfo.Length;
|
||||||
|
public async Task<byte[]> ReadAsync() => _inMemory ??= await File.ReadAllBytesAsync(FileInfo.FullName);
|
||||||
|
public byte[] Read() => _inMemory ??= File.ReadAllBytes(FileInfo.FullName);
|
||||||
|
}
|
||||||
|
internal static class Dotnet {
|
||||||
|
internal static async Task<Result<PackageInfo, ReadableError>> FindPackage(string dir) {
|
||||||
|
var package = Directory.GetFiles(dir, "*.nupkg").FirstOrDefault();
|
||||||
|
if (package == null) return new ReadableError(Exceptions.generic_error.EscapeMarkup());
|
||||||
|
return new PackageInfo(new FileInfo(package)).Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<Result<ShellResult, ReadableError>> Pack(string tempdir, DataReceivedEventHandler? stderr, DataReceivedEventHandler? stdout) {
|
||||||
|
var p = Process.Start(new ProcessStartInfo() {
|
||||||
|
FileName = "dotnet",
|
||||||
|
Arguments = $"pack -o {tempdir}",
|
||||||
|
WorkingDirectory = Environment.CurrentDirectory,
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
});
|
||||||
|
|
||||||
|
StringBuilder errorLines = new();
|
||||||
|
StringBuilder outputLines = new();
|
||||||
|
|
||||||
|
CancellationTokenSource cts = new();
|
||||||
|
p?.ErrorDataReceived += stderr;
|
||||||
|
p?.ErrorDataReceived += (_, e) => {
|
||||||
|
cts.Cancel();
|
||||||
|
errorLines.Append(e.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
p?.OutputDataReceived += stdout;
|
||||||
|
p?.OutputDataReceived += (_, e) => {
|
||||||
|
if (e.Data?.ToLower().Contains("press any key") == true)
|
||||||
|
cts.Cancel();
|
||||||
|
if (e.Data?.ToLower().Contains($"successfully created package '{Path.GetFullPath(tempdir)}") == true) {
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
p?.BeginErrorReadLine();
|
||||||
|
p?.BeginOutputReadLine();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
|
||||||
|
} catch (TaskCanceledException) {
|
||||||
|
p?.Kill();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new ReadableError(Exceptions.generic_error.EscapeMarkup(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ShellResult(outputLines.ToString(), errorLines.ToString(), p!.ExitCode).Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Spectre.Console;
|
||||||
|
using static aeqw89.tools.Publish.Program;
|
||||||
|
|
||||||
|
namespace aeqw89.tools.Publish;
|
||||||
|
|
||||||
|
internal record Stager(RunContext RunContext) {
|
||||||
|
public async Task<T?> Spinner<T>(string name, Func<RunContext, StatusContext, Task<Result<T, ReadableError>>> factory) {
|
||||||
|
try {
|
||||||
|
var result = AnsiConsole.Status()
|
||||||
|
.Spinner(Spectre.Console.Spinner.Known.Dots)
|
||||||
|
.Start(name, async ctx => await factory(RunContext, ctx));
|
||||||
|
return (await result).Unwrap(RunContext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
ShowError(string.Format(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup()));
|
||||||
|
RunContext.Restore();
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnreachableException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProgressStager Progress() {
|
||||||
|
return new ProgressStager(RunContext, [], AnsiConsole.Progress()
|
||||||
|
.AutoClear(true)
|
||||||
|
.HideCompleted(false)
|
||||||
|
.Columns([
|
||||||
|
new TaskDescriptionColumn(),
|
||||||
|
new ProgressBarColumn()
|
||||||
|
.RemainingStyle(Style.Parse("dim gray slowblink"))
|
||||||
|
.CompletedStyle(Style.Parse("green strikethrough"))
|
||||||
|
.FinishedStyle("green strikethrough"),
|
||||||
|
new DownloadedColumn(),
|
||||||
|
new RemainingTimeColumn(),
|
||||||
|
new TransferSpeedColumn(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal record TaskWithProgress<T>(string Name, Task<T> Value);
|
||||||
|
internal record ProgressStager(RunContext RunContext, Task[] Tasks, Progress Progress) {
|
||||||
|
public TaskWithProgress<T> Run<T>(string Name, Func<RunContext, ProgressContext, Task<T>> factory) {
|
||||||
|
return new(Name, Progress.StartAsync(async ctx => {
|
||||||
|
return await factory(RunContext, ctx);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace System.Runtime.CompilerServices {
|
||||||
|
public interface IUnion { }
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class UnionAttribute : Attribute { }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user