// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // shutdown.go — Comprehensive VM shutdown sequence. // // Implements Harbour-compatible shutdown order: // 1. EXIT PROCEDURE execution // 2. AtExit callbacks // 3. All WorkAreas close (child before parent) // 4. Index flush and close // 5. File handles close // 6. Terminal restore (raw → normal) // 7. Static variables clear // 8. Signal handler cleanup // // Also adds Five-specific: // 9. Go object cleanup (GoCall cache clear) // 10. DEFER stack execution // 11. Goroutine drain package hbrt import ( "fmt" "os" "os/signal" "sync" "syscall" ) // --- AtExit registry --- var ( atExitFuncs []func() atExitMu sync.Mutex signalSetup sync.Once shutdownOnce sync.Once ) // AtExit registers a function to be called during VM shutdown. // Functions execute in LIFO order (last registered = first executed). // Harbour: hb_vmAtExit() func AtExit(fn func()) { atExitMu.Lock() atExitFuncs = append(atExitFuncs, fn) atExitMu.Unlock() } // runAtExit executes all registered AtExit functions in reverse order. func runAtExit() { atExitMu.Lock() fns := make([]func(), len(atExitFuncs)) copy(fns, atExitFuncs) atExitFuncs = nil atExitMu.Unlock() // LIFO: last registered, first executed for i := len(fns) - 1; i >= 0; i-- { safeCall(fns[i]) } } // --- Signal handling --- // signalDone closes to stop the signal handler goroutine on normal exit. var signalDone chan struct{} // InstallSignalHandlers sets up OS signal handlers for clean shutdown. // Harbour: hb_vmSetExceptionHandler (SIGINT, SIGTERM, SIGSEGV) func (vm *VM) InstallSignalHandlers() { signalSetup.Do(func() { sigCh := make(chan os.Signal, 1) signalDone = make(chan struct{}) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) go func() { select { case sig := <-sigCh: fmt.Fprintf(os.Stderr, "\nFive: received %v, shutting down...\n", sig) vm.Shutdown() os.Exit(1) case <-signalDone: // Normal exit — stop goroutine cleanly signal.Stop(sigCh) return } }() }) } // stopSignalHandler stops the signal handler goroutine (called during normal shutdown). func stopSignalHandler() { if signalDone != nil { select { case <-signalDone: // already closed default: close(signalDone) } } } // --- Main shutdown sequence --- // Shutdown executes the full VM shutdown sequence. // Harbour: hb_vmQuit() — 25 steps in correct order. // Safe to call multiple times (runs only once). func (vm *VM) Shutdown() { shutdownOnce.Do(func() { vm.doShutdown() }) } func (vm *VM) doShutdown() { // Phase 1: Execute EXIT PROCEDUREs // Harbour: hb_vmDoExitFunctions() vm.runExitProcedures() // Phase 2: Execute AtExit callbacks (user-registered cleanup) // Harbour: hb_vmDoModuleExitFunctions() runAtExit() // Phase 3: Close all WorkAreas (databases, indexes) // Harbour: hb_rddCloseAll() — child before parent vm.closeAllWorkAreas() // Phase 4: Clear memvars // Harbour: hb_memvarsClear() vm.clearMemvars() // Phase 5: Clear static variables // Harbour: hb_vmStaticsClear() — clears complex items vm.clearStatics() // Phase 6: Terminal restore // Harbour: hb_conRelease() → hb_gtExit() vm.restoreTerminal() // Phase 7: User onExit callback if vm.onExit != nil { safeCall(vm.onExit) } // Phase 8: Release class system // Harbour: hb_clsReleaseAll() // (Five: Go GC handles this) // Phase 9: Go GC — force collection // Harbour: hb_gcCollectAll(HB_TRUE) × 3 // Go's GC is automatic, but we can hint // runtime.GC() — not calling explicitly, Go handles it // Phase 10: Stop signal handler goroutine (prevent leak) stopSignalHandler() } // runExitProcedures executes all EXIT PROCEDURE declarations. // Harbour: scans symbols for HB_FS_EXIT scope, executes in reverse module order. func (vm *VM) runExitProcedures() { // Collect EXIT procedures under lock, execute without lock vm.mu.RLock() var exitFuncs []func(*Thread) for i := len(vm.modules) - 1; i >= 0; i-- { m := vm.modules[i] for j := range m.Symbols { sym := &m.Symbols[j] if sym.Scope&FsExit != 0 && sym.Func != nil { exitFuncs = append(exitFuncs, sym.Func) } } } vm.mu.RUnlock() for _, fn := range exitFuncs { safeCallThread(vm, fn) } } // closeAllWorkAreas closes all open database work areas. // Harbour: hb_rddCloseAll() — respects parent-child order. func (vm *VM) closeAllWorkAreas() { vm.mu.RLock() threads := make([]*Thread, len(vm.threads)) copy(threads, vm.threads) vm.mu.RUnlock() for _, t := range threads { if t.WA != nil { if closer, ok := t.WA.(interface{ CloseAll() }); ok { safeCall(func() { closer.CloseAll() }) } } } } // clearMemvars releases all PUBLIC/PRIVATE memvars on all threads. func (vm *VM) clearMemvars() { vm.mu.RLock() threads := make([]*Thread, len(vm.threads)) copy(threads, vm.threads) vm.mu.RUnlock() for _, t := range threads { if t.Memvars != nil { t.Memvars.ReleaseAll() } } } // clearStatics clears all static variables. // Harbour: hb_vmStaticsClear() — clears complex items only. func (vm *VM) clearStatics() { vm.mu.Lock() defer vm.mu.Unlock() for k := range vm.statics { for i := range vm.statics[k] { vm.statics[k][i] = MakeNil() } } } // restoreTerminal restores terminal to normal mode. // Harbour: hb_conRelease() → hb_gtExit() → tcsetattr(saved_TIO) func (vm *VM) restoreTerminal() { // Restore from raw mode if set if restoreFunc := getTerminalRestoreFunc(); restoreFunc != nil { safeCall(restoreFunc) } } // --- Terminal restore hook --- var ( termRestoreFunc func() termRestoreMu sync.Mutex ) // SetTerminalRestore registers the terminal restore function. // Called by hbrtl.InitRawTerminal(). func SetTerminalRestore(fn func()) { termRestoreMu.Lock() termRestoreFunc = fn termRestoreMu.Unlock() } func getTerminalRestoreFunc() func() { termRestoreMu.Lock() defer termRestoreMu.Unlock() return termRestoreFunc } // --- Helpers --- // safeCall executes fn and recovers from panics. func safeCall(fn func()) { defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stderr, "Five shutdown: panic in cleanup: %v\n", r) } }() fn() } // safeCallThread creates a thread and safely calls a function. func safeCallThread(vm *VM, fn func(*Thread)) { defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stderr, "Five shutdown: panic in EXIT proc: %v\n", r) } }() t := vm.NewThread() fn(t) }