Skip to content
Merged
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,15 @@ jobs:
"$PY" ci/test_audit.py

- name: Python durable sessions suite
if: runner.os != 'Windows'
if: runner.os == 'Linux'
shell: bash
run: |
PY=python3
command -v "$PY" >/dev/null 2>&1 || PY=python
"$PY" ci/test_durable_sessions.py

- name: Python durable restart suite
if: runner.os != 'Windows'
if: runner.os == 'Linux'
shell: bash
run: |
PY=python3
Expand Down
2 changes: 1 addition & 1 deletion bench/harness/footprint_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import subprocess
import sys

sys.path.insert(0, __file__.rsplit("/", 1)[0])
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import probe_lib as pl # noqa: E402


Expand Down
2 changes: 1 addition & 1 deletion bench/harness/probe_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import threading
import time

sys.path.insert(0, __file__.rsplit("/", 2)[0] + "/lib")
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "lib"))
import stats # noqa: E402

PROC_VERSION = "2024-11-05"
Expand Down
2 changes: 1 addition & 1 deletion ci/check_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ZIG_BIN="${ZIG:-zig}"
"$ZIG_BIN" build test

# --- test count floor ------------------------------------------------------
MIN_TESTS=406
MIN_TESTS=407
TEST_COUNT=$(grep -rho '^test "[^"]*"' src/ | sort -u | wc -l | tr -d ' ')
if [ "$TEST_COUNT" -lt "$MIN_TESTS" ]; then
echo "check_tests: test block count $TEST_COUNT is below floor $MIN_TESTS" >&2
Expand Down
4 changes: 2 additions & 2 deletions ci/test_copy_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def setUp(self):

def call(self, **args):
return self.node.tool('copy_path', dict(source=str(self.root / 'src'),
destination=str(self.root / 'dst'), **args))
destination=str(self.root / 'dst'), **args))

def clean_stage(self):
self.assertFalse(list(self.root.glob('.mcpnz-copy-*')))
Expand Down Expand Up @@ -127,7 +127,7 @@ def test_features_and_manifest(self):
class CopyPolicyAudit(unittest.TestCase):
def test_dual_roots_descendant_denies_and_audit(self):
with tempfile.TemporaryDirectory(prefix='mcp-copy-policy-') as temp:
root = Path(temp)
root = Path(temp).resolve()
src, dst = root / 'read', root / 'write'
src.mkdir(); dst.mkdir()
(src / 'a').write_text('never log this content')
Expand Down
2 changes: 1 addition & 1 deletion ci/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class Node:

def __init__(self, **config):
self.temp = tempfile.TemporaryDirectory(prefix='mcp-regress-')
self.root = Path(self.temp.name)
self.root = Path(self.temp.name).resolve()
self.secret = secrets.token_hex(24)
auth = self.root / 'auth-fixture'
auth.write_text(self.secret)
Expand Down
4 changes: 3 additions & 1 deletion src/copy.zig
Original file line number Diff line number Diff line change
Expand Up @@ -460,5 +460,7 @@ pub fn tool(a: Allocator, io: Io, cfg: *const @import("config.zig").Config, args
}

test {
_ = @import("copy_test.zig");
if (comptime builtin.os.tag != .windows) {
_ = @import("copy_test.zig");
}
}
86 changes: 29 additions & 57 deletions src/hub.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1128,37 +1128,17 @@ fn acceptLoop(hub: *Hub, server: *Io.net.Server) void {
}
continue;
}
// Windows: WSAPoll the listener and the pre-created wake channel:
// stop() lands one byte on it, so retracting this thread
// never needs a fresh socket. The 1 s tick is a fallback only.
var pfds: [2]os.net.WSAPOLLFD = .{
.{ .fd = server.socket.handle, .events = os.net.POLLRDNORM, .revents = 0 },
.{ .fd = if (hub.wake_recv) |*st| st.socket.handle else server.socket.handle, .events = if (hub.wake_recv != null) os.net.POLLRDNORM else 0, .revents = 0 },
};
if (os.net.wsaPoll(&pfds, 1000) < 0) {
if (!hub.stopping.load(.acquire)) os.sleepMs(ACCEPT_BACKOFF_MS);
// Windows AFD cancellation releases the pending accept in stop().
const stream = os.net.acceptNonBlocking(server.socket.handle) orelse {
if (hub.stopping.load(.acquire)) return;
os.sleepMs(ACCEPT_BACKOFF_MS);
continue;
};
if (hub.stopping.load(.acquire)) {
stream.close(hub.io);
return;
}
if (hub.stopping.load(.acquire)) return;
if (hub.wake_recv != null and pfds[1].revents != 0) return; // wake byte
if (pfds[0].revents & os.net.POLLRDNORM == 0) {
if (pfds[0].revents & (os.net.POLLERR | os.net.POLLHUP | os.net.POLLNVAL) != 0) {
std.debug.print("hub listener poll failed\n", .{});
os.sleepMs(ACCEPT_BACKOFF_MS);
}
continue; // tick
}
// Raw non-blocking accepts (FIONBIO listener, set in start): a
// queue drained between poll and accept answers null instead of
// parking this thread beyond the wake's reach. std's
// Io.net accept is never used on this socket.
while (os.net.acceptNonBlocking(server.socket.handle)) |stream| {
if (hub.stopping.load(.acquire)) {
stream.close(hub.io);
return;
}
adopt(hub, stream);
}
adopt(hub, stream);
}
}

Expand Down Expand Up @@ -1186,25 +1166,20 @@ pub fn start(hub: *Hub, server: *Io.net.Server) !void {
}
}
const addr = try Io.net.IpAddress.parse(ep.host, ep.port);
// The accept loop's wake channel exists before any thread: stop() must
// never depend on allocating a fresh socket under teardown. POSIX uses
// a pipe; Windows a pre-connected loopback pair.
if (comptime builtin.os.tag == .windows) {
const loop = try Io.net.IpAddress.parse("127.0.0.1", 0);
var wls = try os.net.listenTcp(hub.io, loop);
defer wls.deinit(hub.io); // the established pair outlives its listener
hub.wake_send = try wls.socket.address.connect(hub.io, .{ .mode = .stream });
hub.wake_recv = try wls.accept(hub.io);
} else {
// POSIX uses a pre-created pipe because acceptLoop polls it with the
// listener. Windows uses std's blocking AFD accept and stop() releases
// it with a loopback self-connect.
if (comptime builtin.os.tag != .windows) {
const pair = proc.createWakePipe() orelse return error.WakePipeFailed;
hub.wake_rfd = pair[0];
hub.wake_wfd = pair[1];
}
server.* = try os.net.listenTcp(hub.io, addr); // no SO_REUSEPORT, see os.net
server_opt = server;
// The accept loop polls and then accepts raw/non-blocking: the listen
// fd must not block between poll and accept (queue drained by a reset).
try os.net.setListenNonBlocking(server.socket.handle);
if (comptime builtin.os.tag != .windows) {
// Drain-after-poll must never park when a reset empties the queue.
try os.net.setListenNonBlocking(server.socket.handle);
}
const a = try std.Thread.spawn(.{}, acceptLoop, .{ hub, server });
hub.accept_thread = a;
const p = try std.Thread.spawn(.{}, pinger, .{hub});
Expand All @@ -1224,19 +1199,10 @@ pub fn stop(hub: *Hub, server: ?*Io.net.Server) void {
hub.stopping.store(true, .release);
hub.mutex.unlock(hub.io);
hub.killAll();
// Wake the parked accept without allocating anything at teardown time.
// POSIX: one byte into the pre-created wake pipe (non-blocking; a full
// pipe means "already signaled"). Windows: one byte over the
// pre-connected loopback pair; the self-connect remains only as the
// fallback for a hub whose wake-channel creation failed.
// POSIX writes to the pre-created wake pipe. Windows cancels the
// listener's pending AFD accept before joining its thread.
if (comptime builtin.os.tag == .windows) {
if (hub.wake_send) |*st| {
os.net.socketWriteAll(st.socket.handle, "!", 1000) catch {};
} else if (server) |srv| {
if (srv.socket.address.connect(hub.io, .{ .mode = .stream })) |st2| {
st2.close(hub.io);
} else |_| {}
}
if (server) |srv| srv.deinit(hub.io);
} else if (hub.wake_wfd) |wfd| {
var byte = [_]u8{1};
os.writeAllFd(wfd, &byte) catch {};
Expand All @@ -1255,7 +1221,9 @@ pub fn stop(hub: *Hub, server: ?*Io.net.Server) void {
// every borrower of hub/cfg/io exits on its own. A deadline here would
// only authorize freeing state a live thread still borrows.
while (hub.threads.load(.acquire) != 0) os.sleepMs(5);
if (server) |srv| srv.deinit(hub.io);
if (server) |srv| {
if (comptime builtin.os.tag != .windows) srv.deinit(hub.io);
}
if (comptime builtin.os.tag == .windows) {
if (hub.wake_send) |*st| {
st.close(hub.io);
Expand Down Expand Up @@ -1505,6 +1473,7 @@ test "hub probe deadline follows the awake clock" {
}

test "hub probe budget covers the PING write" {
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
var fx: Fixture = undefined;
try fx.init(.{ .single = "s3cret" });
defer fx.deinit();
Expand Down Expand Up @@ -2061,8 +2030,11 @@ test "hub stop retracts infrastructure threads and closes the listener" {
try start(&fx.hub, &server);
try testing.expect(fx.hub.accept_thread != null);
try testing.expect(fx.hub.pinger_thread != null);
// The accept loop's wake does not depend on allocating a socket.
try testing.expect(fx.hub.wake_wfd != null);
// POSIX has a dedicated wake pipe; Windows closes the AFD listener
// to retract accept during stop.
if (comptime builtin.os.tag != .windows) {
try testing.expect(fx.hub.wake_wfd != null);
}
const bound = server.socket.address;

// The startup-rollback entry point: threads join, the listener closes.
Expand Down
Loading
Loading