Skip to content

Commit e2f35df

Browse files
committed
fix(daemon): prevent graceful shutdown hang
1 parent 37a1b61 commit e2f35df

11 files changed

Lines changed: 584 additions & 6 deletions

MCServerLauncher.Daemon/GracefulShutdown.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ namespace MCServerLauncher.Daemon;
44

55
public class GracefulShutdown : IDisposable
66
{
7-
private readonly SemaphoreSlim _semaphore = new(0);
87
private readonly CancellationTokenSource _source = new();
8+
private readonly TaskCompletionSource _shutdownSignal =
9+
new(TaskCreationOptions.RunContinuationsAsynchronously);
910
private bool _isDisposed;
1011

1112
public GracefulShutdown()
@@ -35,18 +36,24 @@ public void Dispose()
3536

3637
public async Task Shutdown()
3738
{
38-
if (_source.IsCancellationRequested)
39-
throw new InvalidOperationException("Already shutdown");
39+
if (_source.IsCancellationRequested) return;
4040

4141
await _source.CancelAsync();
42+
_shutdownSignal.TrySetResult();
4243
Log.Information("[GracefulShutdown] shutting down...");
4344
if (OnShutdown is not null) await OnShutdown.Invoke();
44-
_semaphore.Release();
4545
}
4646

4747
public async Task WaitForShutdownAsync(int timeout = -1)
4848
{
49-
await _semaphore.WaitAsync(timeout);
49+
var waitTask = _shutdownSignal.Task;
50+
if (timeout < 0)
51+
{
52+
await waitTask;
53+
return;
54+
}
55+
56+
await waitTask.WaitAsync(TimeSpan.FromMilliseconds(timeout));
5057
}
5158

5259
private void Dispose(bool dispose)
@@ -60,4 +67,4 @@ private void Dispose(bool dispose)
6067
{
6168
Dispose(false);
6269
}
63-
}
70+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using MCServerLauncher.Daemon;
2+
3+
namespace MCServerLauncher.ProtocolTests;
4+
5+
public class GracefulShutdownTests
6+
{
7+
[Fact]
8+
[Trait("Category", "Daemon")]
9+
[Trait("Category", "Shutdown")]
10+
public async Task WaitForShutdownAsync_CompletesBeforeBlockedShutdownCallbacksFinish()
11+
{
12+
using var gracefulShutdown = new GracefulShutdown();
13+
var callbackCanFinish = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
14+
var callbackStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
15+
16+
gracefulShutdown.OnShutdown += async () =>
17+
{
18+
callbackStarted.SetResult();
19+
await callbackCanFinish.Task;
20+
};
21+
22+
var shutdownTask = gracefulShutdown.Shutdown();
23+
await callbackStarted.Task.WaitAsync(TimeSpan.FromSeconds(2));
24+
25+
await gracefulShutdown.WaitForShutdownAsync().WaitAsync(TimeSpan.FromMilliseconds(200));
26+
27+
callbackCanFinish.SetResult();
28+
await shutdownTask.WaitAsync(TimeSpan.FromSeconds(2));
29+
}
30+
31+
[Fact]
32+
[Trait("Category", "Daemon")]
33+
[Trait("Category", "Shutdown")]
34+
public async Task Shutdown_CalledMoreThanOnce_DoesNotThrow()
35+
{
36+
using var gracefulShutdown = new GracefulShutdown();
37+
38+
await gracefulShutdown.Shutdown();
39+
await gracefulShutdown.Shutdown();
40+
41+
await gracefulShutdown.WaitForShutdownAsync().WaitAsync(TimeSpan.FromMilliseconds(200));
42+
}
43+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Daemon API doc log hint implementation plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Print the Postman collection URL when the daemon starts.
6+
7+
**Architecture:** Keep the existing startup log style in `Application.ServeAsync`. Add one information log line after the HTTP server line that points to `/postman_collection.json`.
8+
9+
**Tech Stack:** C# 14, .NET 10, Serilog, TouchSocket HTTP.
10+
11+
---
12+
13+
## Touched areas
14+
15+
- `backend`
16+
- `docs`
17+
- `tests`
18+
19+
## Tasks
20+
21+
### Task 1: Test
22+
23+
- [x] Add a protocol test requiring the daemon startup log to mention `/postman_collection.json`.
24+
25+
### Task 2: Implementation
26+
27+
- [x] Add the startup log line in `Application.ServeAsync`.
28+
29+
### Task 3: Verify
30+
31+
- [x] Run the focused inbound test.
32+
- [x] Build daemon.
33+
- [x] Run diff whitespace check.
34+
35+
## Changelog
36+
37+
- 2026-06-19: Planned startup log hint for the Postman API documentation URL.
38+
- 2026-06-19: Added the startup API docs log line and verified the Postman collection remains complete.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Daemon API docs startup log implementation plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Print the Postman collection URL when the daemon starts.
6+
7+
**Architecture:** Keep the existing WebSocket and HTTP startup logs. Add one adjacent log line that points to `/postman_collection.json`, the only machine-readable API documentation artifact.
8+
9+
**Tech Stack:** C# 14, .NET 10, Serilog, protocol tests.
10+
11+
---
12+
13+
## Touched areas
14+
15+
- `backend`
16+
- `docs`
17+
- `tests`
18+
19+
## Tasks
20+
21+
### Task 1: Lock log expectation
22+
23+
- [ ] Add a source-inspection protocol test requiring the startup log to mention `/postman_collection.json`.
24+
25+
### Task 2: Add log line
26+
27+
- [ ] Add `Log.Information` after the HTTP server startup log.
28+
29+
### Task 3: Verify
30+
31+
- [ ] Run focused inbound tests.
32+
- [ ] Build daemon.
33+
- [ ] Run `git diff --check`.
34+
35+
## Changelog
36+
37+
- 2026-06-19: Planned a daemon startup log line for the Postman API documentation URL.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Daemon Embedded Swagger Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Embed the current protocol OpenAPI and related protocol docs into the daemon and expose an offline Swagger UI over the existing TouchSocket HTTP plugin.
6+
7+
**Architecture:** The daemon keeps using TouchSocket `HttpPlugin`; no ASP.NET Core or Swashbuckle pipeline is introduced. Protocol docs and Swagger UI files are packaged as `EmbeddedResource` items and served through explicit `GET` routes.
8+
9+
**Tech Stack:** C# 14, .NET 10, TouchSocket HTTP, embedded resources, Swagger UI static assets.
10+
11+
---
12+
13+
## Touched Areas
14+
15+
- `docs`
16+
- `agent-docs`
17+
- `protocol`
18+
- `backend`
19+
- `tests`
20+
21+
## Tasks
22+
23+
### Task 1: Lock Embedded Documentation Expectations
24+
25+
**Files:**
26+
- Modify: `MCServerLauncher.ProtocolTests/DaemonInboundTransportPipelineTests.cs`
27+
28+
- [x] Add source/resource tests that require:
29+
- `openapi.json` to be embedded in `MCServerLauncher.Daemon`.
30+
- Swagger UI static files to be embedded.
31+
- `HttpPlugin` to expose `/openapi.json`, `/swagger`, `/swagger/index.html`, and `/docs/protocol/...`.
32+
33+
### Task 2: Add Embedded Protocol And Swagger Assets
34+
35+
**Files:**
36+
- Create: `MCServerLauncher.Daemon/.Resources/Docs/openapi.json`
37+
- Create: `MCServerLauncher.Daemon/.Resources/Docs/protocol/topics/*.md`
38+
- Create: `MCServerLauncher.Daemon/.Resources/Docs/swagger/index.html`
39+
- Create: `MCServerLauncher.Daemon/.Resources/Docs/swagger/swagger-ui.css`
40+
- Create: `MCServerLauncher.Daemon/.Resources/Docs/swagger/swagger-ui-bundle.js`
41+
- Create: `MCServerLauncher.Daemon/.Resources/Docs/swagger/swagger-ui-standalone-preset.js`
42+
- Create: `MCServerLauncher.Daemon/.Resources/Docs/swagger/LICENSE`
43+
- Modify: `MCServerLauncher.Daemon/MCServerLauncher.Daemon.csproj`
44+
45+
- [x] Copy the current `mcsl-future-protocol` OpenAPI and topic docs into daemon resources.
46+
- [x] Add local Swagger UI assets so `/swagger` works without internet access.
47+
- [x] Embed docs with stable logical names under `MCServerLauncher.Daemon.Resources.Docs.*`.
48+
49+
### Task 3: Serve Embedded Documentation
50+
51+
**Files:**
52+
- Create: `MCServerLauncher.Daemon/Remote/EmbeddedDocumentation.cs`
53+
- Modify: `MCServerLauncher.Daemon/Remote/HttpPlugin.cs`
54+
55+
- [x] Add a small helper that maps request paths to embedded documentation resources and content types.
56+
- [x] Serve `/openapi.json`.
57+
- [x] Serve `/swagger` and `/swagger/index.html`.
58+
- [x] Serve `/swagger/*` static assets.
59+
- [x] Serve `/docs/protocol/*` Markdown resources.
60+
- [x] Keep existing `/`, `/info`, and `/subtoken` behavior unchanged.
61+
62+
### Task 4: Verify
63+
64+
**Commands:**
65+
- [x] `dotnet test MCServerLauncher.ProtocolTests/MCServerLauncher.ProtocolTests.csproj --filter FullyQualifiedName~DaemonInboundTransportPipelineTests /m:1`
66+
- [x] `dotnet build MCServerLauncher.Daemon/MCServerLauncher.Daemon.csproj /m:1`
67+
- [x] `python3 -m json.tool MCServerLauncher.Daemon/.Resources/Docs/openapi.json >/tmp/mcsl-daemon-openapi.json`
68+
- [x] `git diff --check`
69+
70+
## Changelog
71+
72+
- Added plan for embedding protocol docs and offline Swagger UI in the daemon.
73+
- Embedded `openapi.json`, protocol topic Markdown, and offline Swagger UI assets in the daemon.
74+
- Added TouchSocket HTTP routes for `/openapi.json`, `/swagger`, `/swagger/index.html`, `/swagger/*`, and `/docs/protocol/*`.
75+
- Added protocol tests that lock resource embedding and route registration expectations.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Graceful Shutdown Hang Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Prevent daemon shutdown from hanging when shutdown callbacks wait on work that is also waiting for the shutdown signal.
6+
7+
**Architecture:** `GracefulShutdown.Shutdown()` should be idempotent and publish the shutdown signal before awaiting registered cleanup callbacks. `WaitForShutdownAsync()` should observe already-completed shutdowns and should not rely on a single semaphore release that can be lost or delayed by callbacks.
8+
9+
**Tech Stack:** C# 14, .NET 10, xUnit protocol tests.
10+
11+
---
12+
13+
### Task 1: Fix GracefulShutdown Signal Ordering
14+
15+
**Files:**
16+
- Create: `MCServerLauncher.ProtocolTests/GracefulShutdownTests.cs`
17+
- Modify: `MCServerLauncher.Daemon/GracefulShutdown.cs`
18+
19+
- [x] **Step 1: Write the failing tests**
20+
21+
Add tests proving:
22+
23+
- `WaitForShutdownAsync()` completes even when an `OnShutdown` callback is still blocked.
24+
- Calling `Shutdown()` more than once is harmless and does not throw.
25+
26+
- [x] **Step 2: Run focused tests and verify red**
27+
28+
Run: `dotnet test MCServerLauncher.ProtocolTests/MCServerLauncher.ProtocolTests.csproj --filter GracefulShutdownTests /m:1`
29+
30+
Expected before implementation: callback-order test fails because `WaitForShutdownAsync()` waits behind `OnShutdown`.
31+
32+
- [x] **Step 3: Implement minimal fix**
33+
34+
Use a `TaskCompletionSource` as the shutdown signal, set it before invoking callbacks, and make repeat `Shutdown()` calls return without throwing.
35+
36+
- [x] **Step 4: Verify**
37+
38+
Run:
39+
40+
```bash
41+
dotnet test MCServerLauncher.ProtocolTests/MCServerLauncher.ProtocolTests.csproj --filter GracefulShutdownTests /m:1
42+
dotnet test MCServerLauncher.ProtocolTests/MCServerLauncher.ProtocolTests.csproj /m:1
43+
git diff --check
44+
```
45+
46+
Result: focused `GracefulShutdownTests` passed, full protocol tests passed with 343/343 tests, and whitespace check passed.
47+
48+
## Changelog
49+
50+
- 2026-06-19: Planned shutdown signal ordering fix for daemon `GracefulShutdown`.
51+
- 2026-06-19: Fixed shutdown wait ordering by publishing the shutdown signal before awaiting callbacks and making repeated shutdown requests harmless.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Migrate To Apifox Skill Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Create a personal agent skill for migrating API documentation from project source code, OpenAPI, Postman, or Swagger into Apifox project JSON.
6+
7+
**Architecture:** The skill lives under `/Users/lxhtt/.agents/skills/migrate-to-apifox`. `SKILL.md` gives the workflow and trigger rules, `APIFOX_FORMAT.md` records the observed Apifox project shape from `/Users/lxhtt/Downloads/schema-test.apifox.json`, and `scripts/validate-apifox.js` performs deterministic structure checks.
8+
9+
**Tech Stack:** Markdown skill instructions, Apifox project JSON, Node.js validation script using only built-in modules.
10+
11+
---
12+
13+
### Task 1: Skill instructions and format reference
14+
15+
**Files:**
16+
- Create: `/Users/lxhtt/.agents/skills/migrate-to-apifox/SKILL.md`
17+
- Create: `/Users/lxhtt/.agents/skills/migrate-to-apifox/APIFOX_FORMAT.md`
18+
- Create: `/Users/lxhtt/.agents/skills/migrate-to-apifox/scripts/validate-apifox.js`
19+
20+
- [x] **Step 1: Inspect Apifox sample**
21+
22+
Run:
23+
24+
```bash
25+
python3 -m json.tool /Users/lxhtt/Downloads/schema-test.apifox.json >/tmp/schema-test.pretty.json
26+
```
27+
28+
Expected: command exits 0 and reveals root collections such as `apiCollection`, `webSocketCollection`, and `schemaCollection`.
29+
30+
- [x] **Step 2: Write `SKILL.md`**
31+
32+
Include triggers for Apifox migration, source-code extraction, OpenAPI, Swagger, and Postman conversion. Keep the file under 100 lines and link to `APIFOX_FORMAT.md`.
33+
34+
- [x] **Step 3: Write `APIFOX_FORMAT.md`**
35+
36+
Document the project root shape, HTTP API item shape, WebSocket API item shape, schema item shape, and migration rules that prevent WebSocket APIs from being represented as HTTP `GET`.
37+
38+
- [x] **Step 4: Write validation script**
39+
40+
Create `scripts/validate-apifox.js` with checks for:
41+
42+
- root `$schema.app === "apifox"`
43+
- at least one of `apiCollection`, `webSocketCollection`, or `schemaCollection`
44+
- HTTP APIs have lowercase `method`; `api.type` may be omitted in Apifox samples and defaults to HTTP
45+
- webhook items under `apiCollection` are accepted with `api.type === "webhook"`
46+
- WebSocket APIs have no `method`; `api.type` may be omitted in Apifox samples or set to `websocket`
47+
- schema items carry `schema.jsonSchema`
48+
49+
- [x] **Step 5: Validate the skill artifacts**
50+
51+
Run:
52+
53+
```bash
54+
node /Users/lxhtt/.agents/skills/migrate-to-apifox/scripts/validate-apifox.js /Users/lxhtt/Downloads/schema-test.apifox.json
55+
node /Users/lxhtt/.agents/skills/migrate-to-apifox/scripts/validate-apifox.js MCServerLauncher.Daemon/.Resources/Docs/apifox.json
56+
python3 - <<'PY'
57+
from pathlib import Path
58+
p = Path('/Users/lxhtt/.agents/skills/migrate-to-apifox/SKILL.md')
59+
print(len(p.read_text().splitlines()))
60+
PY
61+
git diff --check
62+
```
63+
64+
Expected: validation commands exit 0, `SKILL.md` stays below 100 lines, and `git diff --check` exits 0.
65+
66+
## Changelog
67+
68+
- Created this plan for the `migrate-to-apifox` personal skill.
69+
- Added `migrate-to-apifox` with a concise `SKILL.md`, an Apifox format reference, and a Node.js validation script.
70+
- Validated the script against `/Users/lxhtt/Downloads/schema-test.apifox.json` and `MCServerLauncher.Daemon/.Resources/Docs/apifox.json`.
71+
- Expanded the skill from a narrow HTTP/WebSocket/schema migration helper into a full Apifox project migration skill covering raw socket services, Socket.IO, MCP, custom endpoint protocols, reusable components, auth, tests, and environments.
72+
- Added strict native validation after Apifox imported a generated daemon project with zero recognized APIs. The fix aligns WebSocket items with Apifox's native export shape by omitting `api.type`, while still forbidding `api.method`, and checks HTTP API metadata required by native project imports.
73+
- Updated the daemon Apifox export to match `/Users/lxhtt/Downloads/pingws.json`: WebSocket APIs are nested under `根目录 -> WebSocket actions`, payloads are stored in `api.requestBody.message`, and `api.parameters` carries query/path/cookie/header buckets.
74+
- Updated the daemon Apifox export and skill rules so WebSocket action `api.path` no longer embeds `{{wsUrl}}?token={{token}}`; the environment owns the WebSocket base URL and `token` is documented in `api.parameters.query`.
75+
- Updated dynamic value and native-variable rules: WebSocket request IDs use Apifox dynamic values such as `{{$string.uuid}}`, environment/global variables use native `name` fields, and shared daemon token auth lives in root `commonParameters.parameters.query`.
76+
- Updated daemon Apifox guidance so the project homepage explains how to obtain MainToken, `globalVariables` no longer duplicates `baseUrl` or `wsUrl`, environment variables no longer carry `token`, `permissions`, or `expires`, and users are directed to edit environment management -> global parameters -> Query -> `token`.

0 commit comments

Comments
 (0)