Skip to content

Commit 72fac73

Browse files
committed
0.2.2: fixes for first-capture 400, menu-bar flicker, camera SIGABRT
Server: - When activating a credit-mode session, use clientCapturedAt as started_at (clamped to [serverNow-5min, serverNow]) so upload latency + client clock skew don't push capturedAt microseconds before startedAt and trigger captured_at_before_session_start. - validateCapturedAt tolerates a 2s slop window on that check to handle parallel-activation races. Desktop: - Menu-bar title was flickering between '<1m'/'1m' and '1m'/'2m' because both Rust (1s tick) and JS (per useSessionTimer change) were writing the title with values that drifted by ~1s at the minute boundary. JS now only calls update_tray_time on pause/resume transitions; the Rust ticker is the sole writer. - start_tray_ticker was a sync #[tauri::command] that called tokio::spawn via start_tray_timer. Sync Tauri commands run on threads without a tokio runtime, so spawn panicked and aborted the process. This crashed camera sessions in 0.2.0 + 0.2.1; nobody noticed because screen capture (the common path) goes through start_capture_loop which is already async. Fix: add 'async' to the command. Regression tests assert tokio::spawn behavior in both runtime contexts. SDK (clients/react): no source changes — only the version bump. The 0.2.2 dist (rebuilt locally) contains the credit-mode capturedAt / getNextExpectedAt plumbing landed in 0.2.1; verified downstream desktop + web packages still type-check against the rebuilt bundle.
1 parent c346622 commit 72fac73

16 files changed

Lines changed: 165 additions & 32 deletions

File tree

clients/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lookout/desktop",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"private": true,
55
"type": "module",
66
"scripts": {

clients/desktop/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clients/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ opt-level = 1
66

77
[package]
88
name = "lookout-desktop"
9-
version = "0.2.1"
9+
version = "0.2.2"
1010
edition = "2021"
1111

1212
[lib]

clients/desktop/src-tauri/src/lib.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1628,8 +1628,15 @@ fn stop_capture_loop(state: State<'_, AppState>) -> Result<(), String> {
16281628

16291629
/// Start the Rust-side tray title ticker (for camera sessions where
16301630
/// the capture loop runs in JS but we still want an accurate menu bar timer).
1631+
///
1632+
/// MUST be `async` — `start_tray_timer` calls `tokio::spawn` internally,
1633+
/// which panics if not invoked from inside a tokio runtime. Tauri runs
1634+
/// `async` commands on its own tokio runtime, but sync commands run on a
1635+
/// thread without one. Calling this command sync caused SIGABRT on camera
1636+
/// sessions in 0.2.0 + 0.2.1 (only camera path uses this; screen capture
1637+
/// goes through `start_capture_loop` which is already async).
16311638
#[tauri::command]
1632-
fn start_tray_ticker(
1639+
async fn start_tray_ticker(
16331640
tracked_seconds: i64,
16341641
state: State<'_, AppState>,
16351642
app: AppHandle,
@@ -2296,4 +2303,43 @@ mod compat_tests {
22962303
assert_eq!(parse_iso_to_unix_ms("not-a-date"), None);
22972304
assert_eq!(parse_iso_to_unix_ms("2025/06/01"), None);
22982305
}
2306+
2307+
// ──────────────────────────────────────────────────────────────────
2308+
// Regression test for the camera-session crash (SIGABRT) in 0.2.0/0.2.1.
2309+
//
2310+
// `start_tray_ticker` was a sync `#[tauri::command]` that called
2311+
// `tokio::spawn` via `start_tray_timer`. Sync Tauri commands run on a
2312+
// thread with no tokio runtime in context, so `tokio::spawn` panicked
2313+
// and the app aborted. The fix is making the command `async` so Tauri
2314+
// hosts it on its async runtime.
2315+
//
2316+
// We can't easily instantiate Tauri's AppHandle in a unit test, so we
2317+
// reproduce the underlying invariant: `tokio::spawn` must run inside a
2318+
// runtime. If this assertion ever weakens (e.g. tokio adds an
2319+
// ambient-runtime fallback), revisit whether the `async fn` is still
2320+
// load-bearing on the command.
2321+
// ──────────────────────────────────────────────────────────────────
2322+
2323+
#[test]
2324+
fn tokio_spawn_panics_without_runtime() {
2325+
// The exact failure mode that crashed 0.2.0/0.2.1 camera sessions.
2326+
let result = std::panic::catch_unwind(|| {
2327+
let _ = tokio::spawn(async {});
2328+
});
2329+
assert!(
2330+
result.is_err(),
2331+
"tokio::spawn outside a runtime should panic — if this now succeeds, \
2332+
tokio's behavior changed and the async-fn fix may no longer be required."
2333+
);
2334+
}
2335+
2336+
#[test]
2337+
fn tokio_spawn_succeeds_inside_runtime() {
2338+
// Mirrors what Tauri's async runtime does for `async` commands.
2339+
let rt = tokio::runtime::Runtime::new().expect("build runtime");
2340+
rt.block_on(async {
2341+
let h = tokio::spawn(async { 42i32 });
2342+
assert_eq!(h.await.unwrap(), 42);
2343+
});
2344+
}
22992345
}

clients/desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://raw.githubusercontent.com/nicehash/tauri/refs/tags/tauri-v2.3.1/crates/tauri-utils/schema.json",
33
"productName": "Lookout",
44
"identifier": "com.hackclub.lookout",
5-
"version": "0.2.1",
5+
"version": "0.2.2",
66
"build": {
77
"frontendDist": "../dist",
88
"devUrl": "http://localhost:1420",

clients/desktop/src/components/DesktopRecorder.tsx

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -340,21 +340,42 @@ export function DesktopRecorder({ token, source, onChangeSource: _onChangeSource
340340
return () => unlisten?.();
341341
}, []);
342342

343-
// Sync state to rust backend (for instant tray loads) AND emit to tray window
343+
// Sync menu-bar state to Rust + the tray window.
344+
//
345+
// IMPORTANT: do NOT call `update_tray_time` on every displaySeconds change.
346+
// The Rust tray ticker is the authoritative source for the menu-bar title
347+
// and runs at 1s cadence; calling update_tray_time from JS every second
348+
// creates two writers fighting over the title, which produces the visible
349+
// flicker between "<1m"/"1m" and "1m"/"2m" at the minute boundaries
350+
// (JS interpolates to one second, Rust to another).
351+
//
352+
// We still split the writes:
353+
// - `update_tray_time`: pause/resume only (the icon/title needs to
354+
// change immediately when paused, which the Rust ticker won't do
355+
// within its 1s tick window).
356+
// - `set_tray_state` + `tray-state` event: tray-window UI updates
357+
// fine-grained per second.
358+
359+
// controlMode + initial-show effect (low frequency)
344360
useEffect(() => {
345-
const timeText = formatTimeTray(displaySeconds);
346-
const state = {
347-
displaySeconds,
348-
screenshotCount,
361+
invoke("show_tray", { timeText: formatTimeTray(displaySeconds) }).catch(console.error);
362+
invoke("update_tray_time", {
363+
timeText: formatTimeTray(displaySeconds),
364+
isPaused: controlMode === "paused",
365+
}).catch(console.error);
366+
// (displaySeconds intentionally read-only here — not in deps. We just
367+
// need a sensible initial title; the Rust ticker takes over from there.)
368+
// eslint-disable-next-line react-hooks/exhaustive-deps
369+
}, [controlMode]);
370+
371+
// Per-second state sync (tray window, not menu-bar title)
372+
useEffect(() => {
373+
const state = {
374+
displaySeconds,
375+
screenshotCount,
349376
controlMode,
350-
updatedAt: Date.now()
377+
updatedAt: Date.now(),
351378
};
352-
353-
invoke("show_tray", { timeText }).catch(console.error);
354-
// The Rust tray ticker now handles the menu bar title, but we
355-
// still call update_tray_time as a fallback for the initial render
356-
// and for camera sources where the ticker might not be running yet.
357-
invoke("update_tray_time", { timeText, isPaused: controlMode === "paused" }).catch(console.error);
358379
invoke("set_tray_state", { state }).catch(console.error);
359380
emit("tray-state", state).catch(console.error);
360381

clients/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lookout/react",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"type": "module",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.js",

clients/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lookout/web",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"private": true,
55
"type": "module",
66
"scripts": {

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lookout/server",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"private": true,
55
"type": "module",
66
"main": "./dist/index.js",

0 commit comments

Comments
 (0)