main: handle various forms of process termination more safely

This attempts to solve the problem where, if the rotator is actively
rotating and the program is killed, the LabJack will not be reset and
the rotator will keep running. We install various signal handlers to
try to catch common cases (ctrl+c, terminal getting closed out from
under us). There are still ways to end the process that leave the
LabJack running (such as if it crashes, though there are currently no
known crashes), but I don't think it's possible to completely avoid
that.

The posix signal handling story is a bit ugly. Trying to do pretty much
anything in the asynchronous signal handlers will cause undefined
behavior. Acquiring a mutex and joining a thread are right at the top
of the very long list of things that you cannot do safely in an async
signal handler. One potential solution to this problem is to replace
locks with atomics, which isn't appropriate for our use case (we have
to make sure the controller thread actually has shut down the LabJack
before exiting the process). The other well-known solution is to
manually create a thread that listens for signals synchronously, and
this is the approach taken here.

After having done this, I had the thought that because we are linking
libc anyway, an `atexit` handler might work, but I don't actually know
if it would, and I don't think it's worthwhile to do the work at this
point.
This commit is contained in:
torque 2024-07-31 12:07:29 -07:00
parent c8cfc95938
commit 4895c94d90
Signed by: torque
SSH Key Fingerprint: SHA256:nCrXefBNo6EbjNSQhv0nXmEg/VuNq3sMF5b8zETw3Tk
3 changed files with 105 additions and 24 deletions

View File

@ -10,12 +10,9 @@ const log = std.log.scoped(.RotCtl);
writer: std.io.BufferedWriter(512, std.net.Stream.Writer), writer: std.io.BufferedWriter(512, std.net.Stream.Writer),
running: bool, running: bool,
rotator: YaesuController, rotator: *YaesuController,
pub fn run(allocator: std.mem.Allocator) !void { pub fn run(allocator: std.mem.Allocator) !void {
// var server = std.net.StreamServer.init(.{ .reuse_address = true });
// defer server.deinit();
const listen_addr = try std.net.Address.parseIp( const listen_addr = try std.net.Address.parseIp(
config.rotctl.listen_address, config.rotctl.listen_address,
config.rotctl.listen_port, config.rotctl.listen_port,
@ -30,7 +27,7 @@ pub fn run(allocator: std.mem.Allocator) !void {
var interface: RotCtl = .{ var interface: RotCtl = .{
.writer = undefined, .writer = undefined,
.running = true, .running = true,
.rotator = try YaesuController.init(allocator), .rotator = try YaesuController.create(allocator),
}; };
while (interface.running) { while (interface.running) {

View File

@ -8,6 +8,8 @@ const log = std.log.scoped(.yaesu_controller);
const YaesuController = @This(); const YaesuController = @This();
pub var singleton: ?*YaesuController = null;
control_thread: std.Thread, control_thread: std.Thread,
lock: *std.Thread.Mutex, lock: *std.Thread.Mutex,
controller: *const Controller, controller: *const Controller,
@ -23,7 +25,7 @@ pub const CalibrationRoutine = enum {
}; };
pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !void { pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !void {
const controller = try YaesuController.init(allocator); const controller = try YaesuController.create(allocator);
defer { defer {
controller.quit(); controller.quit();
controller.control_thread.join(); controller.control_thread.join();
@ -35,7 +37,12 @@ pub fn calibrate(allocator: std.mem.Allocator, routine: CalibrationRoutine) !voi
} }
} }
pub fn init(allocator: std.mem.Allocator) !YaesuController { pub fn create(allocator: std.mem.Allocator) !*YaesuController {
if (singleton) |_| {
log.err("Controller singleton already exists.", .{});
return error.AlreadyInitialized;
}
const controller = try allocator.create(Controller); const controller = try allocator.create(Controller);
errdefer allocator.destroy(controller); errdefer allocator.destroy(controller);
controller.* = try Controller.init(allocator); controller.* = try Controller.init(allocator);
@ -43,11 +50,16 @@ pub fn init(allocator: std.mem.Allocator) !YaesuController {
// do this in the main thread so we can throw the error about it synchronously. // do this in the main thread so we can throw the error about it synchronously.
try controller.connectLabjack(); try controller.connectLabjack();
return .{ const self = try allocator.create(YaesuController);
errdefer allocator.destroy(self);
self.* = .{
.control_thread = try std.Thread.spawn(.{}, runController, .{controller}), .control_thread = try std.Thread.spawn(.{}, runController, .{controller}),
.lock = &controller.lock, .lock = &controller.lock,
.controller = controller, .controller = controller,
}; };
singleton = self;
return self;
} }
fn inRange(request: f64, comptime dof: enum { azimuth, elevation }) bool { fn inRange(request: f64, comptime dof: enum { azimuth, elevation }) bool {
@ -88,7 +100,7 @@ pub fn setTarget(self: YaesuController, target: AzEl) error{OutOfRange}!void {
const controller = @constCast(self.controller); const controller = @constCast(self.controller);
controller.target = masked_target; controller.target = masked_target;
controller.requested_state = .running; controller.requestState(.running);
} }
pub fn currentPosition(self: YaesuController) AzEl { pub fn currentPosition(self: YaesuController) AzEl {
@ -114,7 +126,7 @@ pub fn quit(self: YaesuController) void {
defer self.lock.unlock(); defer self.lock.unlock();
const controller = @constCast(self.controller); const controller = @constCast(self.controller);
controller.requested_state = .stopped; controller.requestState(.stopped);
} }
pub fn stop(self: YaesuController) void { pub fn stop(self: YaesuController) void {
@ -123,7 +135,7 @@ pub fn stop(self: YaesuController) void {
const controller = @constCast(self.controller); const controller = @constCast(self.controller);
controller.target = controller.position; controller.target = controller.position;
controller.requested_state = .idle; controller.requestState(.idle);
} }
pub fn startPark(self: YaesuController) void { pub fn startPark(self: YaesuController) void {
@ -254,6 +266,18 @@ const Controller = struct {
self.labjack.id = info.local_id; self.labjack.id = info.local_id;
} }
// this function is run with the lock already acquired
fn propagateState(self: *Controller) void {
if (self.current_state == .stopped) return;
self.current_state = self.requested_state;
}
// this function is run with the lock already acquired
fn requestState(self: *Controller, request: ControllerState) void {
if (self.current_state == .stopped) return;
self.requested_state = request;
}
fn lerpOne(input: f64, cal_points: Config.MinMax) f64 { fn lerpOne(input: f64, cal_points: Config.MinMax) f64 {
return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle; return (input - cal_points.minimum.voltage) * cal_points.slope() + cal_points.minimum.angle;
} }
@ -358,32 +382,26 @@ const Controller = struct {
var timer = LoopTimer.init(config.controller.loop_interval_ns); var timer = LoopTimer.init(config.controller.loop_interval_ns);
while (timer.mark()) : (timer.sleep()) { while (timer.mark()) : (timer.sleep()) {
self.updateFeedback() catch { const fbfail = if (self.updateFeedback()) |_| false else |_| true;
self.lock.lock();
defer self.lock.unlock();
self.current_state = .stopped;
continue;
};
{ {
self.lock.lock(); self.lock.lock();
defer self.lock.unlock(); defer self.lock.unlock();
self.setPosition(self.feedback_buffer.get()); self.setPosition(self.feedback_buffer.get());
if (fbfail) self.requestState(.stopped);
self.propagateState();
} }
switch (self.current_state) { switch (self.current_state) {
.initializing, .idle => { .initializing, .idle => {},
self.current_state = self.requested_state;
},
.calibration => { .calibration => {
self.lock.lock(); self.lock.lock();
defer self.lock.unlock(); defer self.lock.unlock();
// run calibration routine. psych, this does nothing. gottem // run calibration routine. psych, this does nothing. gottem
self.current_state = .idle; self.current_state = .idle;
self.requested_state = self.current_state; self.requestState(.idle);
}, },
.running => { .running => {
const pos_error: AzEl = blk: { const pos_error: AzEl = blk: {

View File

@ -10,6 +10,62 @@ const udev = @import("udev_rules");
const log = std.log.scoped(.main); const log = std.log.scoped(.main);
fn quit() noreturn {
if (YaesuController.singleton) |controller| {
controller.quit();
controller.control_thread.join();
}
std.process.exit(1);
}
const moreposix = struct {
pub extern "c" fn sigaddset(set: *std.posix.sigset_t, signo: c_int) c_int;
pub extern "c" fn sigdelset(set: *std.posix.sigset_t, signo: c_int) c_int;
pub extern "c" fn sigemptyset(set: *std.posix.sigset_t) c_int;
pub extern "c" fn sigfillset(set: *std.posix.sigset_t) c_int;
pub extern "c" fn sigismember(set: *const std.posix.sigset_t, signo: c_int) c_int;
// stdlib prototype is wrong, it doesn't take optional pointers.
pub extern "c" fn pthread_sigmask(how: c_int, noalias set: ?*const std.posix.sigset_t, noalias oldset: ?*std.posix.sigset_t) c_int;
};
const psigs = [_]c_int{ std.posix.SIG.INT, std.posix.SIG.HUP, std.posix.SIG.QUIT };
fn posixSignalHandlerThread() void {
var set: std.posix.sigset_t = undefined;
_ = moreposix.sigemptyset(&set);
for (psigs) |sig|
_ = moreposix.sigaddset(&set, sig);
var sig: c_int = 0;
_ = std.posix.system.sigwait(&set, &sig);
log.info("Got exit signal", .{});
quit();
}
// Windows runs this handler in a thread, so calling quit directly should be safe.
fn windowsEventHandler(code: std.os.windows.DWORD) callconv(std.os.windows.WINAPI) std.os.windows.BOOL {
_ = code;
log.info("Got exit signal", .{});
quit();
}
fn addExitHandler() !void {
if (comptime builtin.os.tag == .windows) {
try std.os.windows.SetConsoleCtrlHandler(windowsEventHandler, true);
} else if (comptime std.Thread.use_pthreads) {
var set: std.posix.sigset_t = undefined;
_ = moreposix.sigemptyset(&set);
for (psigs) |sig|
_ = moreposix.sigaddset(&set, sig);
_ = moreposix.pthread_sigmask(std.posix.SIG.BLOCK, &set, null);
// nobody cares about the thread
_ = try std.Thread.spawn(.{}, posixSignalHandlerThread, .{});
} else {
log.err("not windows and not pthreads = disaster", .{});
}
}
fn printStderr(comptime fmt: []const u8, args: anytype) void { fn printStderr(comptime fmt: []const u8, args: anytype) void {
std.debug.print(fmt ++ "\n", args); std.debug.print(fmt ++ "\n", args);
} }
@ -66,9 +122,14 @@ pub fn main() !u8 {
defer Config.deinit(); defer Config.deinit();
addExitHandler() catch {
log.err("Could not install quit handler.", .{});
return 1;
};
RotCtl.run(allocator) catch |err| { RotCtl.run(allocator) catch |err| {
log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)}); log.err("rotator controller ceased unexpectedly! {s}", .{@errorName(err)});
return 1; quit();
}; };
} else if (std.mem.eql(u8, args[1], commands.calibrate)) { } else if (std.mem.eql(u8, args[1], commands.calibrate)) {
if (args.len < 3 or args.len > 4) { if (args.len < 3 or args.len > 4) {
@ -86,9 +147,14 @@ pub fn main() !u8 {
return 1; return 1;
}; };
addExitHandler() catch {
log.err("Could not install quit handler.", .{});
return 1;
};
YaesuController.calibrate(allocator, routine) catch |err| { YaesuController.calibrate(allocator, routine) catch |err| {
log.err("Calibration failed: {s}", .{@errorName(err)}); log.err("Calibration failed: {s}", .{@errorName(err)});
return 1; quit();
}; };
} else if (std.mem.eql(u8, args[1], commands.help)) { } else if (std.mem.eql(u8, args[1], commands.help)) {
if (args.len != 3) { if (args.len != 3) {