feat: Harbour-compatible VM shutdown sequence

Implements full cleanup on program exit (normal, Ctrl+C, crash):
- EXIT PROCEDURE auto-execution (reverse module order)
- AtExit callback registry (LIFO order)
- All WorkAreas auto-close (child before parent)
- Terminal restore (raw → normal) on signal/exit
- Static variables clear
- Signal handlers (SIGINT, SIGTERM) for clean shutdown
- shutdown.go: Harbour hb_vmQuit() 25-step sequence adapted for Five
- vm.go: Run() now calls Shutdown() via defer
- rawtty.go: terminal restore registered with shutdown system

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 10:07:42 +09:00
parent d774f1598c
commit 272576f6ce
5 changed files with 286 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
{
"version": "2.0",
"lastUpdated": "2026-03-30T23:17:34.469Z",
"lastUpdated": "2026-03-31T01:06:49.182Z",
"activeFeatures": [
"hbrt",
"hbrtl",
@@ -32,9 +32,9 @@
"documents": {},
"timestamps": {
"started": "2026-03-27T09:33:04.512Z",
"lastUpdated": "2026-03-30T23:17:34.469Z"
"lastUpdated": "2026-03-31T01:05:45.128Z"
},
"lastFile": "/mnt/d/charles/five/hbrt/macroeval_test.go"
"lastFile": "/mnt/d/charles/five/hbrt/vm.go"
},
"hbrtl": {
"phase": "do",
@@ -45,9 +45,9 @@
"documents": {},
"timestamps": {
"started": "2026-03-27T11:15:10.675Z",
"lastUpdated": "2026-03-29T13:02:52.259Z"
"lastUpdated": "2026-03-31T01:06:49.182Z"
},
"lastFile": "/mnt/d/charles/five/hbrtl/json_test.go"
"lastFile": "/mnt/d/charles/five/hbrtl/rawtty.go"
},
"tests": {
"phase": "do",
@@ -266,7 +266,7 @@
"session": {
"startedAt": "2026-03-27T06:06:49.620Z",
"onboardingCompleted": false,
"lastActivity": "2026-03-30T23:17:34.469Z"
"lastActivity": "2026-03-31T01:06:49.182Z"
},
"history": [
{
@@ -5290,6 +5290,42 @@
"feature": "hbrt",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-03-31T01:04:07.876Z",
"feature": "hbrt",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-03-31T01:04:54.144Z",
"feature": "hbrt",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-03-31T01:05:27.088Z",
"feature": "hbrt",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-03-31T01:05:45.128Z",
"feature": "hbrt",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-03-31T01:06:16.666Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
},
{
"timestamp": "2026-03-31T01:06:49.182Z",
"feature": "hbrtl",
"phase": "do",
"action": "updated"
}
]
}

229
hbrt/shutdown.go Normal file
View File

@@ -0,0 +1,229 @@
// 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() {
vm.mu.RLock()
defer vm.mu.RUnlock()
// Collect EXIT procedures from all modules (reverse order)
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 {
safeCallThread(vm, sym.Func)
}
}
}
}
// closeAllWorkAreas closes all open database work areas.
// Harbour: hb_rddCloseAll() — respects parent-child order.
func (vm *VM) closeAllWorkAreas() {
vm.mu.RLock()
defer vm.mu.RUnlock()
for _, t := range vm.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)
}

View File

@@ -11,6 +11,7 @@ type VM struct {
modules []*Module
symbols map[string]*Symbol
statics map[string][]Value
threads []*Thread // all threads created (for shutdown cleanup)
waFactory func() interface{} // creates WorkAreaManager for new threads
onExit func() // called when Run() finishes (restore terminal etc.)
Debugger *Debugger // nil = no debugging; set by five debug command
@@ -116,7 +117,11 @@ func (vm *VM) FindSymbol(name string) *Symbol {
// NewThread creates a new Thread attached to this VM.
func (vm *VM) NewThread() *Thread {
return NewThread(vm)
t := NewThread(vm)
vm.mu.Lock()
vm.threads = append(vm.threads, t)
vm.mu.Unlock()
return t
}
// Run starts execution from the named function.
@@ -149,12 +154,11 @@ func (vm *VM) Run(funcName string) Value {
}
vm.mu.RUnlock()
// Call the function, ensure cleanup on exit
defer func() {
if vm.onExit != nil {
vm.onExit()
}
}()
// Install signal handlers for clean shutdown
vm.InstallSignalHandlers()
// Call the function, ensure full shutdown on exit
defer vm.Shutdown()
sym.Func(t)
return t.retVal

View File

@@ -5,6 +5,7 @@
package hbrtl
import (
"five/hbrt"
"os"
"syscall"
"unsafe"
@@ -38,6 +39,9 @@ func InitRawTerminal() {
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(stdinFd), 0x5402, uintptr(unsafe.Pointer(&raw)), 0, 0, 0)
rawModeOn = true
// Register terminal restore with VM shutdown system
hbrt.SetTerminalRestore(RestoreTerminal)
}
// RestoreTerminal restores original terminal.

BIN
test_scope Normal file

Binary file not shown.