feat: complete TASK-WP0-07 supervisor
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Dada.Supervisor;
|
||||
|
||||
internal enum ChildRole
|
||||
{
|
||||
Api,
|
||||
Worker,
|
||||
}
|
||||
|
||||
internal static class CredentialCatalog
|
||||
{
|
||||
internal const string ApiResend = "Dada/P0A/api/resend";
|
||||
internal const string ApiAmap = "Dada/P0A/api/amap";
|
||||
internal const string WorkerAiGateway = "Dada/P0A/worker/ai-gateway";
|
||||
internal const string AdminPepper = "Dada/P0A/admin/pepper";
|
||||
|
||||
internal static IReadOnlyList<string> RequiredFor(ChildRole role) => role switch
|
||||
{
|
||||
ChildRole.Api => [ApiResend, ApiAmap],
|
||||
ChildRole.Worker => [WorkerAiGateway],
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(role)),
|
||||
};
|
||||
}
|
||||
|
||||
internal interface ICredentialStore
|
||||
{
|
||||
string? Read(string target);
|
||||
void Write(string target, string value);
|
||||
void Delete(string target);
|
||||
bool IsConfigured(string target);
|
||||
}
|
||||
|
||||
internal sealed class MissingCredentialException(string target)
|
||||
: InvalidOperationException($"Required credential is not configured: {target}");
|
||||
|
||||
internal static class CredentialProcessLauncher
|
||||
{
|
||||
internal static async Task<Process> StartAsync(
|
||||
ProcessStartInfo startInfo,
|
||||
ChildRole role,
|
||||
ICredentialStore store,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var target in CredentialCatalog.RequiredFor(role))
|
||||
{
|
||||
credentials[target] = store.Read(target) ?? throw new MissingCredentialException(target);
|
||||
}
|
||||
|
||||
startInfo.UseShellExecute = false;
|
||||
startInfo.CreateNoWindow = true;
|
||||
startInfo.RedirectStandardInput = true;
|
||||
var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start managed child process.");
|
||||
var payload = JsonSerializer.SerializeToUtf8Bytes(credentials);
|
||||
try
|
||||
{
|
||||
await process.StandardInput.BaseStream.WriteAsync(payload, cancellationToken);
|
||||
await process.StandardInput.BaseStream.FlushAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!process.HasExited) process.Kill(entireProcessTree: true);
|
||||
process.Dispose();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Array.Clear(payload);
|
||||
process.StandardInput.Close();
|
||||
foreach (var target in credentials.Keys.ToArray()) credentials[target] = string.Empty;
|
||||
credentials.Clear();
|
||||
}
|
||||
return process;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class WindowsCredentialStore : ICredentialStore
|
||||
{
|
||||
public string? Read(string target)
|
||||
{
|
||||
if (!CredRead(target, CredentialType.Generic, 0, out var pointer))
|
||||
{
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
if (error == ErrorNotFound) return null;
|
||||
throw new Win32Exception(error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var credential = Marshal.PtrToStructure<NativeCredential>(pointer);
|
||||
return credential.CredentialBlobSize == 0
|
||||
? string.Empty
|
||||
: Marshal.PtrToStringUni(credential.CredentialBlob, credential.CredentialBlobSize / sizeof(char));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CredFree(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string target, string value)
|
||||
{
|
||||
var bytes = System.Text.Encoding.Unicode.GetBytes(value);
|
||||
if (bytes.Length > MaximumGenericBlobSize) throw new ArgumentOutOfRangeException(nameof(value));
|
||||
var pointer = Marshal.AllocHGlobal(bytes.Length);
|
||||
try
|
||||
{
|
||||
Marshal.Copy(bytes, 0, pointer, bytes.Length);
|
||||
var credential = new NativeCredential
|
||||
{
|
||||
Type = CredentialType.Generic,
|
||||
TargetName = target,
|
||||
CredentialBlobSize = bytes.Length,
|
||||
CredentialBlob = pointer,
|
||||
Persist = CredentialPersistence.LocalMachine,
|
||||
UserName = Environment.UserName,
|
||||
};
|
||||
if (!CredWrite(ref credential, 0)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.Copy(new byte[bytes.Length], 0, pointer, bytes.Length);
|
||||
Marshal.FreeHGlobal(pointer);
|
||||
Array.Clear(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete(string target)
|
||||
{
|
||||
if (CredDelete(target, CredentialType.Generic, 0)) return;
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
if (error != ErrorNotFound) throw new Win32Exception(error);
|
||||
}
|
||||
|
||||
public bool IsConfigured(string target) => Read(target) is not null;
|
||||
|
||||
private const int ErrorNotFound = 1168;
|
||||
private const int MaximumGenericBlobSize = 5 * 512;
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern bool CredRead(string target, CredentialType type, int flags, out IntPtr credential);
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern bool CredWrite([In] ref NativeCredential credential, int flags);
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern bool CredDelete(string target, CredentialType type, int flags);
|
||||
|
||||
[DllImport("Advapi32.dll")]
|
||||
private static extern void CredFree(IntPtr buffer);
|
||||
|
||||
private enum CredentialType : uint { Generic = 1 }
|
||||
private enum CredentialPersistence : uint { LocalMachine = 2 }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct NativeCredential
|
||||
{
|
||||
public uint Flags;
|
||||
public CredentialType Type;
|
||||
public string TargetName;
|
||||
public string? Comment;
|
||||
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
||||
public int CredentialBlobSize;
|
||||
public IntPtr CredentialBlob;
|
||||
public CredentialPersistence Persist;
|
||||
public int AttributeCount;
|
||||
public IntPtr Attributes;
|
||||
public string? TargetAlias;
|
||||
public string UserName;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user