diff --git a/app/root.zig b/app/root.zig index 509f6bd3d..5899cb9de 100644 --- a/app/root.zig +++ b/app/root.zig @@ -218,6 +218,7 @@ pub const store = struct { // whose tests no std-only test root of their own reaches. `Layout`'s own `test` block brings in // the split trees, seeds, regions, drop plans and view drag beside it. test { + _ = update.update_notify; _ = settings.Settings; _ = view.ViewHost; _ = layout.Layout; diff --git a/app/update/bounded_join.zig b/app/update/bounded_join.zig deleted file mode 100644 index cebac162c..000000000 --- a/app/update/bounded_join.zig +++ /dev/null @@ -1,58 +0,0 @@ -//! Waiting for a thread at shutdown for only so long. std-only, so its test needs no window. - -const std = @import("std"); - -/// Waits until `done` is set, up to `bound_ms`, then joins `thread`; past the bound, detaches it -/// instead and returns false. The caller owns what the thread still touches: it must outlive -/// the process or the thread must touch nothing torn down meanwhile. -pub fn joinWithin(io: std.Io, thread: std.Thread, done: *const std.atomic.Value(bool), bound_ms: u32) bool { - const start = std.Io.Clock.awake.now(io); - while (!done.load(.acquire)) { - if (start.durationTo(std.Io.Clock.awake.now(io)).toMilliseconds() >= bound_ms) { - thread.detach(); - return false; - } - io.sleep(.fromMilliseconds(poll_ms), .awake) catch {}; - } - thread.join(); - return true; -} - -const poll_ms = 5; - -test "a stalled thread is detached once the bound passes" { - const Stall = struct { - // The test thread releases the check; the worker publishes entry and completion. - var entered: std.atomic.Value(bool) = .init(false); - var release: std.atomic.Value(bool) = .init(false); - var done: std.atomic.Value(bool) = .init(false); - fn run(io: std.Io) void { - entered.store(true, .release); - while (!release.load(.acquire)) io.sleep(.fromMilliseconds(1), .awake) catch {}; - done.store(true, .release); - } - }; - const io = std.testing.io; - const thread = try std.Thread.spawn(.{}, Stall.run, .{io}); - defer { - Stall.release.store(true, .release); - while (!Stall.done.load(.acquire)) io.sleep(.fromMilliseconds(1), .awake) catch {}; - } - while (!Stall.entered.load(.acquire)) io.sleep(.fromMilliseconds(1), .awake) catch {}; - const start = std.Io.Clock.awake.now(io); - try std.testing.expect(!joinWithin(io, thread, &Stall.done, 50)); - const elapsed = start.durationTo(std.Io.Clock.awake.now(io)); - try std.testing.expect(elapsed.toMilliseconds() < 1000); - try std.testing.expect(!Stall.done.load(.acquire)); -} - -test "a finished thread is joined" { - const Quick = struct { - var done: std.atomic.Value(bool) = .init(false); - fn run() void { - done.store(true, .release); - } - }; - const thread = try std.Thread.spawn(.{}, Quick.run, .{}); - try std.testing.expect(joinWithin(std.testing.io, thread, &Quick.done, 1000)); -} diff --git a/app/update/update_notify.zig b/app/update/update_notify.zig index dec9a5a49..cfb9f5f77 100644 --- a/app/update/update_notify.zig +++ b/app/update/update_notify.zig @@ -9,7 +9,6 @@ const core = @import("core"); const auto_update = @import("auto_update.zig"); const update_install = @import("update_install.zig"); const move_offer = @import("move_offer.zig"); -const bounded_join = @import("bounded_join.zig"); const restart = @import("../restart.zig"); const Phase = enum(u8) { @@ -96,7 +95,7 @@ fn startCheck() void { } // The worker may outlive `deinit` (see there), so it cannot borrow the app's allocator. - check_thread = std.Thread.spawn(.{}, worker, .{}) catch { + check_thread = std.Thread.spawn(.{}, worker, .{ std.heap.page_allocator, auto_update.checkRemoteVersionSummary }) catch { phase.store(@backingInt(Phase.done_no), .release); return; }; @@ -107,7 +106,7 @@ fn startCheck() void { var check_thread: if (auto_update.impl) ?std.Thread else void = if (auto_update.impl) null else {}; /// Set by the check's thread as its last act. -var check_done: std.atomic.Value(bool) = .init(false); +var check_done: std.Io.Event = .unset; /// How long quit waits on a check still in flight: Velopack's request blocks in C and cannot be /// cancelled, so past this the thread is detached and the process exit ends it. @@ -117,19 +116,28 @@ const quit_wait_ms = 250; /// so the one given to `setAllocator` may be torn down after this returns. pub fn deinit(io: std.Io) void { if (comptime !auto_update.impl) return; - if (check_thread) |thread| _ = bounded_join.joinWithin(io, thread, &check_done, quit_wait_ms); + if (check_thread) |thread| { + if (check_done.waitTimeout(io, .{ .duration = .{ .raw = .fromMilliseconds(quit_wait_ms), .clock = .awake } })) |_| { + thread.join(); + } else |_| { + // Timeout or cancellation must both let shutdown proceed. + thread.detach(); + } + } check_thread = null; } -fn worker() void { - defer check_done.store(true, .release); +fn worker( + gpa: std.mem.Allocator, + check: *const fn (std.Io, std.mem.Allocator, []u8) error{OutOfMemory}!auto_update.CheckSummary, +) void { // A detached check must also outlive teardown of the application's Io. - const gpa = std.heap.page_allocator; var threaded: std.Io.Threaded = .init(gpa, .{}); defer threaded.deinit(); const io = threaded.io(); + defer check_done.set(io); var ver_buf: [128]u8 = undefined; - const summary = auto_update.checkRemoteVersionSummary(io, gpa, &ver_buf) catch { + const summary = check(io, gpa, &ver_buf) catch { phase.store(@backingInt(Phase.done_no), .release); return; }; @@ -424,3 +432,22 @@ fn displayProgressToast(id: dvui.Id) !void { animator.data().min_size = .{}; } } + +test "launch worker signals completion on success and error" { + const Check = struct { + fn success(_: std.Io, _: std.mem.Allocator, _: []u8) error{OutOfMemory}!auto_update.CheckSummary { + return .no_update; + } + fn failure(_: std.Io, _: std.mem.Allocator, _: []u8) error{OutOfMemory}!auto_update.CheckSummary { + return error.OutOfMemory; + } + }; + defer check_done = .unset; + defer phase.store(@backingInt(Phase.pending), .release); + for ([_]@TypeOf(&Check.success){ &Check.success, &Check.failure }) |check| { + check_done = .unset; + worker(std.testing.allocator, check); + try std.testing.expect(check_done.isSet()); + try std.testing.expectEqual(AppUpdateState.none, appUpdateState()); + } +} diff --git a/build/app.zig b/build/app.zig index 15d8a8c2f..7c8fb0af5 100644 --- a/build/app.zig +++ b/build/app.zig @@ -1108,8 +1108,6 @@ fn addTests( // /Applications, and what happens to the copy it leaves. std-only (see // install_location.zig); `macos_bundle.zig` asks the system and acts on it. .{ "fizzy-install-location-tests", "app/update/install_location.zig" }, - // Quit's bounded wait on the launch update check's thread. std-only (see bounded_join.zig). - .{ "fizzy-bounded-join-tests", "app/update/bounded_join.zig" }, // The command palette's recently used commands: order, re-use, the cap. std-only // (see RecentCommands.zig); `Recents` holds it and stores it in recents.zon. .{ "fizzy-recent-commands-tests", "app/RecentCommands.zig" },