Skip to content

Commit 74eee75

Browse files
committed
Add a synchronous signal handler for graceful shutdown
1 parent c962858 commit 74eee75

File tree

4 files changed

+149
-56
lines changed

4 files changed

+149
-56
lines changed

src/app.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub const App = struct {
1919
telemetry: Telemetry,
2020
app_dir_path: ?[]const u8,
2121
notification: *Notification,
22+
shutdown: bool = false,
2223

2324
pub const RunMode = enum {
2425
help,
@@ -82,9 +83,14 @@ pub const App = struct {
8283
}
8384

8485
pub fn deinit(self: *App) void {
86+
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
87+
return;
88+
}
89+
8590
const allocator = self.allocator;
8691
if (self.app_dir_path) |app_dir_path| {
8792
allocator.free(app_dir_path);
93+
self.app_dir_path = null;
8894
}
8995
self.telemetry.deinit();
9096
self.notification.deinit();

src/main.zig

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,67 +23,42 @@ const Allocator = std.mem.Allocator;
2323
const log = @import("log.zig");
2424
const App = @import("app.zig").App;
2525
const Server = @import("server.zig").Server;
26+
const SigHandler = @import("sighandler.zig").SigHandler;
2627
const Browser = @import("browser/browser.zig").Browser;
2728
const DumpStripMode = @import("browser/dump.zig").Opts.StripMode;
2829

2930
const build_config = @import("build_config");
3031

31-
var _app: ?*App = null;
32-
var _server: ?Server = null;
33-
3432
pub fn main() !void {
3533
// allocator
3634
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
3735
// - in Release mode we use the c allocator
38-
var gpa: std.heap.DebugAllocator(.{}) = .init;
39-
const alloc = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;
36+
var gpa_instance: std.heap.DebugAllocator(.{}) = .init;
37+
const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;
4038

4139
defer if (builtin.mode == .Debug) {
42-
if (gpa.detectLeaks()) std.posix.exit(1);
40+
if (gpa_instance.detectLeaks()) std.posix.exit(1);
4341
};
4442

45-
run(alloc) catch |err| {
43+
var arena_instance = std.heap.ArenaAllocator.init(gpa);
44+
const arena = arena_instance.allocator();
45+
defer arena_instance.deinit();
46+
47+
var sighandler = SigHandler{ .arena = arena };
48+
try sighandler.install();
49+
50+
run(gpa, arena, &sighandler) catch |err| {
4651
// If explicit filters were set, they won't be valid anymore because
47-
// the args_arena is gone. We need to set it to something that's not
48-
// invalid. (We should just move the args_arena up to main)
52+
// the arena is gone. We need to set it to something that's not
53+
// invalid. (We should just move the arena up to main)
4954
log.opts.filter_scopes = &.{};
5055
log.fatal(.app, "exit", .{ .err = err });
5156
std.posix.exit(1);
5257
};
5358
}
5459

55-
// Handle app shutdown gracefuly on signals.
56-
fn shutdown() void {
57-
const sigaction: std.posix.Sigaction = .{
58-
.handler = .{
59-
.handler = struct {
60-
pub fn handler(_: c_int) callconv(.c) void {
61-
// Shutdown service gracefuly.
62-
if (_server) |server| {
63-
server.deinit();
64-
}
65-
if (_app) |app| {
66-
app.deinit();
67-
}
68-
std.posix.exit(0);
69-
}
70-
}.handler,
71-
},
72-
.mask = std.posix.empty_sigset,
73-
.flags = 0,
74-
};
75-
// Exit the program on SIGINT signal. When running the browser in a Docker
76-
// container, sending a CTRL-C (SIGINT) signal is catched but doesn't exit
77-
// the program. Here we force exiting on SIGINT.
78-
std.posix.sigaction(std.posix.SIG.INT, &sigaction, null);
79-
std.posix.sigaction(std.posix.SIG.TERM, &sigaction, null);
80-
std.posix.sigaction(std.posix.SIG.QUIT, &sigaction, null);
81-
}
82-
83-
fn run(alloc: Allocator) !void {
84-
var args_arena = std.heap.ArenaAllocator.init(alloc);
85-
defer args_arena.deinit();
86-
const args = try parseArgs(args_arena.allocator());
60+
fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void {
61+
const args = try parseArgs(arena);
8762

8863
switch (args.mode) {
8964
.help => {
@@ -110,13 +85,13 @@ fn run(alloc: Allocator) !void {
11085
const user_agent = blk: {
11186
const USER_AGENT = "User-Agent: Lightpanda/1.0";
11287
if (args.userAgentSuffix()) |suffix| {
113-
break :blk try std.fmt.allocPrintSentinel(args_arena.allocator(), "{s} {s}", .{ USER_AGENT, suffix }, 0);
88+
break :blk try std.fmt.allocPrintSentinel(arena, "{s} {s}", .{ USER_AGENT, suffix }, 0);
11489
}
11590
break :blk USER_AGENT;
11691
};
11792

11893
// _app is global to handle graceful shutdown.
119-
_app = try App.init(alloc, .{
94+
var app = try App.init(gpa, .{
12095
.run_mode = args.mode,
12196
.http_proxy = args.httpProxy(),
12297
.proxy_bearer_token = args.proxyBearerToken(),
@@ -127,8 +102,6 @@ fn run(alloc: Allocator) !void {
127102
.http_max_concurrent = args.httpMaxConcurrent(),
128103
.user_agent = user_agent,
129104
});
130-
131-
const app = _app.?;
132105
defer app.deinit();
133106
app.telemetry.record(.{ .run = {} });
134107

@@ -141,10 +114,11 @@ fn run(alloc: Allocator) !void {
141114
};
142115

143116
// _server is global to handle graceful shutdown.
144-
_server = try Server.init(app, address);
145-
const server = &_server.?;
117+
var server = try Server.init(app, address);
146118
defer server.deinit();
147119

120+
try sighandler.on(Server.stop, .{&server});
121+
148122
// max timeout of 1 week.
149123
const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000;
150124
server.run(address, timeout) catch |err| {

src/server.zig

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;
3838

3939
pub const Server = struct {
4040
app: *App,
41-
shutdown: bool,
41+
shutdown: bool = false,
4242
allocator: Allocator,
4343
client: ?posix.socket_t,
4444
listener: ?posix.socket_t,
@@ -53,16 +53,36 @@ pub const Server = struct {
5353
.app = app,
5454
.client = null,
5555
.listener = null,
56-
.shutdown = false,
5756
.allocator = allocator,
5857
.json_version_response = json_version_response,
5958
};
6059
}
6160

61+
/// Interrupts the server so that main can complete normally and call all defer handlers.
62+
pub fn stop(self: *Server) void {
63+
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
64+
return;
65+
}
66+
67+
// Linux and BSD/macOS handle canceling a socket blocked on accept differently.
68+
// For Linux, we use std.shutdown, which will cause accept to return error.SocketNotListening (EINVAL).
69+
// For BSD, shutdown will return an error. Instead we call posix.close, which will result with error.ConnectionAborted (BADF).
70+
if (self.listener) |listener| switch (builtin.target.os.tag) {
71+
.linux => posix.shutdown(listener, .recv) catch |err| {
72+
log.warn(.app, "listener shutdown", .{ .err = err });
73+
},
74+
.macos, .freebsd, .netbsd, .openbsd => {
75+
self.listener = null;
76+
posix.close(listener);
77+
},
78+
else => unreachable,
79+
};
80+
}
81+
6282
pub fn deinit(self: *Server) void {
63-
self.shutdown = true;
6483
if (self.listener) |listener| {
6584
posix.close(listener);
85+
self.listener = null;
6686
}
6787
// *if* server.run is running, we should really wait for it to return
6888
// before existing from here.
@@ -83,14 +103,19 @@ pub const Server = struct {
83103
try posix.listen(listener, 1);
84104

85105
log.info(.app, "server running", .{ .address = address });
86-
while (true) {
106+
while (!@atomicLoad(bool, &self.shutdown, .monotonic)) {
87107
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| {
88-
if (self.shutdown) {
89-
return;
108+
switch (err) {
109+
error.SocketNotListening, error.ConnectionAborted => {
110+
log.info(.app, "server stopped", .{});
111+
break;
112+
},
113+
else => {
114+
log.err(.app, "CDP accept", .{ .err = err });
115+
std.Thread.sleep(std.time.ns_per_s);
116+
continue;
117+
},
90118
}
91-
log.err(.app, "CDP accept", .{ .err = err });
92-
std.Thread.sleep(std.time.ns_per_s);
93-
continue;
94119
};
95120

96121
self.client = socket;

src/sighandler.zig

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//! This structure processes operating system signals (SIGINT, SIGTERM)
2+
//! and runs callbacks to clean up the system gracefully.
3+
//!
4+
//! The structure does not clear the memory allocated in the arena,
5+
//! clear the entire arena when exiting the program.
6+
const std = @import("std");
7+
const assert = std.debug.assert;
8+
const Allocator = std.mem.Allocator;
9+
10+
const log = @import("log.zig");
11+
12+
pub const SigHandler = struct {
13+
arena: Allocator,
14+
15+
sigset: std.posix.sigset_t = undefined,
16+
handle_thread: ?std.Thread = null,
17+
18+
attempt: u32 = 0,
19+
listeners: std.ArrayList(Listener) = .empty,
20+
21+
pub const Listener = struct {
22+
args: []const u8,
23+
start: *const fn (context: *const anyopaque) void,
24+
};
25+
26+
pub fn install(self: *SigHandler) !void {
27+
// Block SIGINT and SIGTERM for the current thread and all created from it
28+
self.sigset = std.posix.sigemptyset();
29+
std.posix.sigaddset(&self.sigset, std.posix.SIG.INT);
30+
std.posix.sigaddset(&self.sigset, std.posix.SIG.TERM);
31+
std.posix.sigaddset(&self.sigset, std.posix.SIG.QUIT);
32+
std.posix.sigprocmask(std.posix.SIG.BLOCK, &self.sigset, null);
33+
34+
self.handle_thread = try std.Thread.spawn(.{ .allocator = self.arena }, SigHandler.sighandle, .{self});
35+
self.handle_thread.?.detach();
36+
}
37+
38+
pub fn on(self: *SigHandler, func: anytype, args: std.meta.ArgsTuple(@TypeOf(func))) !void {
39+
assert(@typeInfo(@TypeOf(func)).@"fn".return_type.? == void);
40+
41+
const Args = @TypeOf(args);
42+
const TypeErased = struct {
43+
fn start(context: *const anyopaque) void {
44+
const args_casted: *const Args = @ptrCast(@alignCast(context));
45+
@call(.auto, func, args_casted.*);
46+
}
47+
};
48+
49+
const buffer = try self.arena.alignedAlloc(u8, .of(Args), @sizeOf(Args));
50+
errdefer self.arena.free(buffer);
51+
52+
const bytes: []const u8 = @ptrCast((&args)[0..1]);
53+
@memcpy(buffer, bytes);
54+
55+
try self.listeners.append(self.arena, .{
56+
.args = buffer,
57+
.start = TypeErased.start,
58+
});
59+
}
60+
61+
fn sighandle(self: *SigHandler) noreturn {
62+
while (true) {
63+
var sig: c_int = 0;
64+
65+
const rc = std.c.sigwait(&self.sigset, &sig);
66+
if (rc != 0) {
67+
log.err(.app, "Unable to process signal {}", .{rc});
68+
std.process.exit(1);
69+
}
70+
71+
switch (sig) {
72+
std.posix.SIG.INT, std.posix.SIG.TERM => {
73+
if (self.attempt > 1) {
74+
std.process.exit(1);
75+
}
76+
self.attempt += 1;
77+
78+
log.info(.app, "Received termination signal...", .{});
79+
for (self.listeners.items) |*item| {
80+
item.start(item.args.ptr);
81+
}
82+
continue;
83+
},
84+
else => continue,
85+
}
86+
}
87+
}
88+
};

0 commit comments

Comments
 (0)