Complete Harbour-compatible MEMVAR implementation: - PUBLIC: global scope, persist until program end - PRIVATE: function scope + called functions, auto-release on return - Shadowing: PRIVATE can shadow PUBLIC, restored on scope exit - Nested: multi-level PRIVATE scoping with save/restore stack - Thread.PushMemvar/PopMemvar: stack-based memvar access - Thread.DeclarePublic/DeclarePrivate: declaration helpers - MacroEval: &cVar now looks up memvars (was returning string) - Shutdown: Phase 4 clears all memvars on all threads - Case-insensitive: all lookups uppercased Tests: 12 tests including: PUBLIC create/update, case-insensitive, PRIVATE basic, shadow/restore, nested 3-level shadow, new var cleanup, release, releaseAll, names, thread integration, macro access Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
275 lines
6.4 KiB
Go
275 lines
6.4 KiB
Go
// 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)
|
||
}
|