Improved logic.

This commit is contained in:
qwsdcvghyu89
2026-06-04 16:45:35 +10:00
parent 115801e161
commit d2f2f671a4
5 changed files with 341 additions and 0 deletions
+184
View File
@@ -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();
}
}
+37
View File
@@ -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);
}
}
+67
View File
@@ -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();
}
}
}
+47
View File
@@ -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);
}));
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace System.Runtime.CompilerServices {
public interface IUnion { }
[AttributeUsage(AttributeTargets.Class)]
public sealed class UnionAttribute : Attribute { }
}