cron: remove all unsafe from Bun.cron

Merged
JA
3mo ago
+489-441

What

Stacked on #40055 (uses its bun_ptr / host-export generator additions). Takes src/runtime/api/cron.rs from 34 unsafe sites to zero by fixing the layers below rather than annotating call sites:

  • CronRegisterJob / CronRemoveJob own themselves through a RefPtr released in finish (was heap::into_raw + heap::take); finish is one shared CronJobBase method.
  • The spawned crontab/launchctl/schtasks child is a bun_spawn::ProcessHandle (owning ref; Drop = detach + deref; typed set_exit_handler<T: ProcessExitOwner>), and argv/env are &[&CStr] end to end via a new safe bun_spawn::spawn_process_cstr(options, argv, SpawnEnv) — no raw *const c_char arrays or CStr::from_ptr in the caller.
  • The promise reactions are HOST_EXPORTs using a new generator shape: an impl of the form fn(this: ThisPtr<T>, &JSGlobalObject, &CallFrame) -> JsResult<JSValue> gets a thunk that hands back the ctx registered with JSValue::then as a typed ThisPtr (replaces the hand-written jsc_host_abi! shims and as_promise_ptr + ThisPtr::new at each reaction; reusable for the other ~20 as_promise_ptr sites).
  • The per-VM job list moves from an erased Vec<*mut c_void> in bun_jsc::RareData to RuntimeState::cron_jobs: Vec<RefPtr<CronJob>>; the pending-promise ref is JsCell<Option<RefPtr<Self>>> instead of a bool + manual ref_()/deref(); stop() reaches the ThisPtr paths through a self_ref back-reference set at allocation; FakeTimers hands stop_dropped_from_fake_heap a ThisPtr.

Testing

Debug+ASAN, cronie installed: cron.test.ts 82 pass / 35 skip (Windows-only), in-process-cron.test.ts 27 pass, cron-local-time.test.ts 30 pass; cron-parse.test.ts unchanged (its <50ms case fails on every debug build). By hand: PATH=/nonexistent register/remove reject; register → crontab -l → remove round-trip; stop-inside-callback and pending-promise re-arm under fake timers; worker terminate with a pending cron promise; --hot reload ×3. rust-check-all for x86_64-pc-windows-msvc and aarch64-apple-darwin passes.