Files
five/hbrt/shutdown.go
Charles KWON OhJun 2c812885c3 feat: MEMVAR system — PUBLIC/PRIVATE dynamic variables
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>
2026-04-02 15:03:34 +09:00

275 lines
6.4 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 ---
// 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)
}