Files
five/hbrt/shutdown.go
Charles KWON OhJun 6b37cc19e4 test: 8 shutdown tests — EXIT, AtExit, WorkArea, panic-safe, statics
Tests verify:
- EXIT PROCEDURE auto-execution on shutdown
- Reverse module order for EXIT
- AtExit LIFO callback order
- WorkArea CloseAll on exit
- Panic in cleanup doesn't crash (safeCall)
- Shutdown runs exactly once (sync.Once)
- Static variables cleared
- Full sequence order: EXIT → AtExit → WA:Close → onExit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:12:34 +09:00

236 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 ---
// InstallSignalHandlers sets up OS signal handlers for clean shutdown.
// Harbour: hb_vmSetExceptionHandler (SIGINT, SIGTERM, SIGSEGV)
func (vm *VM) InstallSignalHandlers() {
signalSetup.Do(func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-ch
fmt.Fprintf(os.Stderr, "\nFive: received %v, shutting down...\n", sig)
vm.Shutdown()
os.Exit(1)
}()
})
}
// --- 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()
// (Five: memvars stored in thread, cleared with thread)
// 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
}
// 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() })
}
}
}
}
// 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)
}