Initial WebhookServer implementation
Add the .NET 8 solution scaffolded against PLAN.md. Three projects share WebhookServer.Core (models, auth, execution, storage, IPC, callbacks) and WebhookServer.Service hosts an embedded Kestrel listener plus the named-pipe admin server. WebhookServer.Gui is a thin MVVM client over the pipe. Includes 25 unit tests covering HMAC verification, bearer auth, IP allowlist parsing, arg-template rendering, DPAPI round-trip, and the encrypt-on-save config store. Install/uninstall PowerShell scripts default to LocalSystem and accept a domain user or gMSA via -ServiceAccount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace WebhookServer.Core.Execution;
|
||||
|
||||
/// <summary>
|
||||
/// Holds one <see cref="SemaphoreSlim"/> per endpoint. When an endpoint is configured
|
||||
/// with Serialize=true, the executor must acquire its semaphore before running and
|
||||
/// release after — guaranteeing at-most-one concurrent run per endpoint.
|
||||
/// </summary>
|
||||
public sealed class ConcurrencyGate
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, SemaphoreSlim> _gates = new();
|
||||
|
||||
public async Task<IDisposable> AcquireAsync(Guid endpointId, CancellationToken ct)
|
||||
{
|
||||
var sem = _gates.GetOrAdd(endpointId, _ => new SemaphoreSlim(1, 1));
|
||||
await sem.WaitAsync(ct).ConfigureAwait(false);
|
||||
return new Releaser(sem);
|
||||
}
|
||||
|
||||
public void Forget(Guid endpointId)
|
||||
{
|
||||
if (_gates.TryRemove(endpointId, out var sem))
|
||||
sem.Dispose();
|
||||
}
|
||||
|
||||
private sealed class Releaser : IDisposable
|
||||
{
|
||||
private SemaphoreSlim? _sem;
|
||||
public Releaser(SemaphoreSlim sem) => _sem = sem;
|
||||
public void Dispose()
|
||||
{
|
||||
var sem = Interlocked.Exchange(ref _sem, null);
|
||||
sem?.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user