Files
five/hbrtl/errorlog.go
CharlesKWON f4ed42556b checkpoint: season-wide bug fix campaign + infra
Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2
SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved
as a single checkpoint before refactoring the parser to delegate xBase
command translation to the preprocessor.

Highlights:

FiveSql2 engine (_FiveSql2/src/)
- prefix-glob index attach -> explicit convention (<table>_pk.ntx,
  <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop
- DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt)
- COUNT(DISTINCT col) parsed + aggregated via hSeen hash
- UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent)
- DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT)
- Derived table FROM (SELECT...) + JOIN right-side derived
- Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect
- LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs)
- DATE literal round-trip validation (Feb 29 non-leap rejected)
- CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists
- AlterTable type dispatcher comma-wrapped (1-char type "A" no longer
  matches CHARACTER)

Compiler / runtime
- gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity)
- gengo split: emit_block.go, emit_stmt.go, folding.go extracted
- parser/stmtreg.go nudges
- hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*),
  windows debug stubs collapsed
- thread/vm/value/class/pcinterp tightening from panic traces

RDD layer (hbrdd/)
- dbf: null bitmap support (null.go + null_test.go), mmap split
  (mmap_posix.go / mmap_windows.go), byte-level numeric parse
- ntx/cdx: windows mmap parity
- workarea + mem RDD: cross-area state-bleed fixes

RTL (hbrtl/)
- errorlog rewrite with platform-specific FD (errorlog_fd_unix /
  errorlog_fd_other)
- sqlscan, sqlhelpers, indexrtl, datetime extensions

Gates green at checkpoint:
- go test ./...        : PASS
- FiveSql2 SQL:1999    : 43/43
- Harbour compat       : 56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:26:25 +09:00

780 lines
21 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Rich diagnostic error-log writer — Harbour FiveWin ErrSysW.prg style.
//
// Produces a structured `error.log` when an unhandled error fires.
// Sections:
// 1. Application: exe path, size, Go/OS version, start time, error time
// 2. Error: description, operation, subsystem, gencode, args
// 3. Stack: full call stack (function, source, line)
// 4. Workareas: every open area's alias, RecNo, RecCount, BOF, EOF,
// DELETED, active index tag, field list
// 5. Classes: every registered class name
// 6. Runtime: goroutines, MemStats, CPU count
//
// Wire-in from PRG:
// ErrorBlock({|e| HB_ErrorLog(e), Break(e)})
//
// Control:
// HB_SetErrorLogPath(cPath) — default "./error.log"
// HB_SetErrorLogHook(bBlock) — receive (oError, cLogPath) after write
//
// Reference: harbour-core/contrib/FiveWin/source/ErrSysW.prg
package hbrtl
import (
"fmt"
"os"
"os/user"
"runtime"
"runtime/debug"
"sort"
"strings"
"sync"
"time"
"five/hbrdd"
"five/hbrt"
)
var (
errLogMu sync.Mutex
errLogPath = "error.log"
errLogHook hbrt.Value // optional block: receives (oErr, cPath)
errLogStartAt = time.Now()
)
// Sensitive-key detection. Substring match on upper-cased key/arg name —
// false positives are acceptable, false negatives (leaked secret) are not.
// PWD is deliberately excluded — it collides with the Unix env var for
// "present working directory", which we want to keep visible.
var redactPatterns = []string{
"PASSWORD", "PASSWD",
"SECRET", "TOKEN", "CREDENTIAL", "AUTH",
"APIKEY", "API_KEY", "ACCESS_KEY", "PRIVATE_KEY",
"SESSION", "COOKIE", "BEARER",
}
// Env vars safe to include — anything outside this list is dropped even
// if it doesn't match a redact pattern. Rationale: a senior engineer
// needs locale/path info; nothing else is worth the leakage risk.
var envAllowlist = []string{
"PATH", "LANG", "LC_ALL", "LC_CTYPE", "LC_TIME", "LC_NUMERIC",
"TZ", "HOME", "USER", "LOGNAME", "SHELL", "TERM", "PWD",
"GOMAXPROCS", "GOGC", "GOTRACEBACK", "GOOS", "GOARCH",
"FIVE_KEEP_BUILD", "HB_LANG",
}
func isSensitiveKey(k string) bool {
u := strings.ToUpper(k)
for _, p := range redactPatterns {
if strings.Contains(u, p) {
return true
}
}
return false
}
// redactArg masks the value portion of --flag=value / -flag=value when
// the flag name looks sensitive. Plain positional args that happen to
// look like secrets are left alone — we can't know without context.
func redactArg(a string) string {
for _, sep := range []string{"=", ":"} {
if idx := strings.Index(a, sep); idx > 0 {
key := strings.TrimLeft(a[:idx], "-")
if isSensitiveKey(key) {
return a[:idx+1] + "***REDACTED***"
}
}
}
return a
}
// Previous-errors ring buffer — cascading failures usually have a
// precursor. Five entries is enough for most debugging sessions without
// bloating the log.
type ringEntry struct {
ts time.Time
desc string
op string
where string
}
var (
errRingMu sync.Mutex
errRing []ringEntry
)
const errRingSize = 5
func recordError(desc, op, where string) {
errRingMu.Lock()
defer errRingMu.Unlock()
if len(errRing) >= errRingSize {
errRing = errRing[1:]
}
errRing = append(errRing, ringEntry{time.Now(), desc, op, where})
}
func snapshotErrRing() []ringEntry {
errRingMu.Lock()
defer errRingMu.Unlock()
out := make([]ringEntry, len(errRing))
copy(out, errRing)
return out
}
// init installs Five's default error handler and the debugger's
// diagnostic renderer. Any *HbError that escapes Main — array OOB,
// type mismatch, divide-by-zero, etc. — triggers an error.log dump
// without the PRG having to wire ErrorBlock. Matches Harbour/FiveWin's
// ErrorSys/ErrSysW default behavior. The diagnostic hook reuses the
// same section writers so the debugger's `diag` command shows
// error.log content at the break point.
func init() {
hbrt.DebugDiagnosticHook = func(t *hbrt.Thread, section string, emit func(string)) {
var b strings.Builder
switch section {
case "wa":
writeWorkareas(&b, t)
case "set":
writeSetState(&b)
case "mem":
writeRuntime(&b, time.Now())
default:
emit("-- Workareas --")
writeWorkareas(&b, t)
emit(b.String())
b.Reset()
emit("-- SET state --")
writeSetState(&b)
emit(b.String())
b.Reset()
emit("-- Runtime --")
writeRuntime(&b, time.Now())
emit(b.String())
return
}
emit(b.String())
}
hbrt.DefaultErrorHook = func(t *hbrt.Thread, r interface{}) {
var oErr hbrt.Value
var stack []hbrt.DebugStackFrame
var desc, op string
switch v := r.(type) {
case *hbrt.HbError:
oErr = hbErrorToHash(v)
stack = v.Stack
desc, op = v.Description, v.Operation
case BreakValue:
oErr = v.Value
desc = describe(v.Value)
default:
oErr = hbrt.MakeString(fmt.Sprintf("%v", r))
desc = fmt.Sprintf("%v", r)
}
// Record the error in the ring buffer BEFORE writing the log so
// the "Previous errors" section includes this one as the tail.
where := "(unknown)"
if len(stack) > 0 {
where = fmt.Sprintf("%s (%s:%d)", stack[0].Function, stack[0].Module, stack[0].Line)
}
recordError(desc, op, where)
body := buildErrorLog(t, oErr, stack)
errLogMu.Lock()
path := errLogPath
errLogMu.Unlock()
if werr := os.WriteFile(path, []byte(body), 0o644); werr != nil {
fmt.Fprintf(os.Stderr, "hb_errorlog: failed to write %s: %v\n", path, werr)
return
}
fmt.Fprintf(os.Stderr, "error logged to %s\n", path)
}
}
// hbErrorToHash lifts the runtime's lightweight *HbError into the same
// hash shape used by ErrorNew / FiveWin's oErr, so the log writer has a
// uniform input.
func hbErrorToHash(e *hbrt.HbError) hbrt.Value {
h := &hbrt.HbHash{}
add := func(k string, v hbrt.Value) {
h.Keys = append(h.Keys, hbrt.MakeString(k))
h.Values = append(h.Values, v)
}
add("DESCRIPTION", hbrt.MakeString(e.Description))
add("OPERATION", hbrt.MakeString(e.Operation))
add("SUBSYSTEM", hbrt.MakeString(e.SubSystem))
add("GENCODE", hbrt.MakeInt(e.GenCode))
add("SUBCODE", hbrt.MakeInt(0))
add("SEVERITY", hbrt.MakeInt(2))
add("OSCODE", hbrt.MakeInt(0))
if len(e.Args) > 0 {
add("ARGS", hbrt.MakeArrayFrom(e.Args))
}
h.Order = make([]int, len(h.Keys))
for i := range h.Order {
h.Order[i] = i
}
return hbrt.MakeHashFrom(h)
}
// HB_SetErrorLogPath(cPath) → cOldPath
func HbSetErrorLogPath(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
errLogMu.Lock()
old := errLogPath
if nParams >= 1 && !t.Local(1).IsNil() {
errLogPath = t.Local(1).AsString()
}
errLogMu.Unlock()
t.RetString(old)
}
// HB_SetErrorLogHook(bBlock) → bOldBlock
func HbSetErrorLogHook(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
errLogMu.Lock()
old := errLogHook
if nParams >= 1 && t.Local(1).IsBlock() {
errLogHook = t.Local(1)
}
errLogMu.Unlock()
if old.IsNil() || !old.IsBlock() {
t.RetNil()
} else {
t.RetVal(old)
}
}
// HB_ErrorLog(oError) → cLogPath
//
// Writes a full diagnostic dump for oError and returns the path. Callers
// typically wire it through ErrorBlock:
//
// ErrorBlock({|e| HB_ErrorLog(e), Break(e)})
func HbErrorLog(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
var oErr hbrt.Value
if nParams >= 1 {
oErr = t.Local(1)
} else {
oErr = hbrt.MakeNil()
}
errLogMu.Lock()
path := errLogPath
hook := errLogHook
errLogMu.Unlock()
body := buildErrorLog(t, oErr, nil)
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
// Log-write failures should not crash the program — dump to stderr
// so the operator sees something.
fmt.Fprintf(os.Stderr, "hb_errorlog: failed to write %s: %v\n", path, err)
}
// User-supplied post-action — e.g. send to a remote endpoint, show a
// dialog, etc. Block receives (oErr, cPath).
if hook.IsBlock() {
t.PushValue(hook)
t.PushValue(oErr)
t.PushString(path)
t.PendingParams2(2)
hook.AsBlock().Fn(t)
_ = t.Pop2() // discard block return
}
t.RetString(path)
}
// buildErrorLog is pure-text composition so it can be unit-tested without
// actually writing to disk. If preStack is non-nil it overrides the live
// call stack — used by the DefaultErrorHook to show the PRG stack as it
// was at the moment of panic (before BEGIN SEQUENCE / EndProc unwound it).
func buildErrorLog(t *hbrt.Thread, oErr hbrt.Value, preStack []hbrt.DebugStackFrame) string {
var b strings.Builder
nowTs := time.Now()
sect := func(title string) {
b.WriteString("\n")
b.WriteString(title)
b.WriteString("\n")
b.WriteString(strings.Repeat("=", len(title)))
b.WriteString("\n")
}
// --- Application ---
sect("Application")
exe, _ := os.Executable()
fmt.Fprintf(&b, " Path and name: %s\n", exe)
if fi, err := os.Stat(exe); err == nil {
fmt.Fprintf(&b, " Size: %s bytes\n", addCommas(fi.Size()))
fmt.Fprintf(&b, " Built at: %s\n", fi.ModTime().Format("2006-01-02 15:04:05"))
}
fmt.Fprintf(&b, " Five runtime: Go %s, %s/%s\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH)
if bi, ok := debug.ReadBuildInfo(); ok {
fmt.Fprintf(&b, " Module: %s\n", bi.Main.Path)
if bi.Main.Version != "" && bi.Main.Version != "(devel)" {
fmt.Fprintf(&b, " Version: %s\n", bi.Main.Version)
}
if rev := buildSetting(bi, "vcs.revision"); rev != "" {
mod := buildSetting(bi, "vcs.modified")
dirty := ""
if mod == "true" {
dirty = " (dirty)"
}
fmt.Fprintf(&b, " VCS: %s%s\n", rev, dirty)
}
}
host, _ := os.Hostname()
fmt.Fprintf(&b, " Host: %s, PID: %d\n", host, os.Getpid())
elapsed := nowTs.Sub(errLogStartAt)
fmt.Fprintf(&b, " Time from start: %s\n", elapsed.Round(time.Millisecond))
fmt.Fprintf(&b, " Error occurred at: %s\n", nowTs.Format("2006-01-02 15:04:05.000"))
// --- Error description ---
sect("Error")
writeErrorSection(&b, oErr)
// --- Stack trace ---
sect("Stack Calls")
frames := preStack
if frames == nil {
frames = t.DebugCallStack()
}
if len(frames) == 0 {
b.WriteString(" (no stack info available)\n")
}
for i, f := range frames {
fmt.Fprintf(&b, " [%3d] %s (%s:%d)\n", i, f.Function, f.Module, f.Line)
}
// --- Workareas ---
sect("Workareas")
writeWorkareas(&b, t)
// --- SET state ---
sect("SET state")
writeSetState(&b)
// --- Classes ---
sect("Classes in use")
names := hbrt.ListClassNames()
sort.Strings(names)
for i, n := range names {
fmt.Fprintf(&b, " [%3d] %s\n", i+1, n)
}
if len(names) == 0 {
b.WriteString(" (no classes registered)\n")
}
// --- Previous errors ---
sect("Previous errors")
writePrevErrors(&b)
// --- Environment ---
sect("Environment")
writeEnvironment(&b)
// --- Runtime ---
sect("Runtime")
writeRuntime(&b, nowTs)
// --- Goroutine dump (only if the PRG actually spawned concurrency) ---
// Baseline is 3: main + signal handler + shutdown watcher. We only
// care when user code produced extra goroutines (hb_Thread*, channels,
// etc.), which is where concurrency bugs actually live.
if runtime.NumGoroutine() > 3 {
sect("Goroutines")
writeGoroutineDump(&b)
}
return b.String()
}
func buildSetting(bi *debug.BuildInfo, key string) string {
for _, s := range bi.Settings {
if s.Key == key {
return s.Value
}
}
return ""
}
// writeSetState dumps the SET values a senior engineer actually looks
// at when reproducing an error: date handling, deleted filter, string
// comparison mode, open-mode default.
func writeSetState(b *strings.Builder) {
fmt.Fprintf(b, " DATEFORMAT: %s\n", GetSetDateFormat())
fmt.Fprintf(b, " EPOCH: %d\n", GetSetEpoch())
fmt.Fprintf(b, " DELETED: %v (filter-out hidden records)\n", GetSetDeleted())
fmt.Fprintf(b, " EXACT: %v\n", GetSetExact())
fmt.Fprintf(b, " SOFTSEEK: %v\n", GetSetSoftSeek())
fmt.Fprintf(b, " DECIMALS: %d\n", GetSetDecimals())
}
// writePrevErrors prints the ring buffer of recent errors. The current
// error is recorded as the last entry, so the list doubles as a
// "errors leading to this one" trace.
func writePrevErrors(b *strings.Builder) {
entries := snapshotErrRing()
if len(entries) == 0 {
b.WriteString(" (none)\n")
return
}
for i, e := range entries {
age := time.Since(e.ts).Round(time.Millisecond)
fmt.Fprintf(b, " [%d] -%s %s (op: %s) at %s\n",
i+1, age, e.desc, e.op, e.where)
}
}
// writeEnvironment writes non-secret context a senior engineer needs to
// tell dev-vs-prod apart. Sensitive env vars are filtered via an
// allowlist; command-line args are redacted on flag name match.
func writeEnvironment(b *strings.Builder) {
if cwd, err := os.Getwd(); err == nil {
fmt.Fprintf(b, " CWD: %s\n", cwd)
}
if u, err := user.Current(); err == nil {
fmt.Fprintf(b, " User: %s (uid=%s, gid=%s)\n", u.Username, u.Uid, u.Gid)
}
fmt.Fprintf(b, " TZ: %s\n", time.Now().Format("MST -0700"))
if len(os.Args) > 0 {
fmt.Fprintf(b, " Args (%d):\n", len(os.Args))
for i, a := range os.Args {
fmt.Fprintf(b, " [%d] %s\n", i, redactArg(a))
}
}
// Environment: allowlist only; anything else (including sensitive
// unknowns) is dropped on the floor.
shown := 0
for _, k := range envAllowlist {
if v, ok := os.LookupEnv(k); ok {
if shown == 0 {
b.WriteString(" Env (allowlisted):\n")
}
if isSensitiveKey(k) {
v = "***REDACTED***"
}
fmt.Fprintf(b, " %s=%s\n", k, v)
shown++
}
}
}
// writeRuntime dumps Go scheduler + GC state. GC pause totals often
// reveal "the error happened because GC was running 30% of the time"
// class problems.
func writeRuntime(b *strings.Builder, nowTs time.Time) {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Fprintf(b, " NumCPU: %d, GOMAXPROCS: %d, NumGoroutine: %d\n",
runtime.NumCPU(), runtime.GOMAXPROCS(0), runtime.NumGoroutine())
fmt.Fprintf(b, " Alloc: %s, TotalAlloc: %s, Sys: %s\n",
addCommas(int64(ms.Alloc)), addCommas(int64(ms.TotalAlloc)),
addCommas(int64(ms.Sys)))
fmt.Fprintf(b, " HeapObjects: %d, HeapInuse: %s\n",
ms.HeapObjects, addCommas(int64(ms.HeapInuse)))
fmt.Fprintf(b, " NumGC: %d, PauseTotal: %s, GCCPUFraction: %.4f%%\n",
ms.NumGC, time.Duration(ms.PauseTotalNs).Round(time.Microsecond),
ms.GCCPUFraction*100)
if ms.NumGC > 0 {
lastGC := time.Unix(0, int64(ms.LastGC))
fmt.Fprintf(b, " Last GC: %s ago\n",
nowTs.Sub(lastGC).Round(time.Millisecond))
}
if n := openFDCount(); n >= 0 {
fmt.Fprintf(b, " Open FDs: %d\n", n)
}
}
// writeGoroutineDump captures runtime.Stack — only invoked when > 1
// goroutine is alive, since for single-threaded PRG programs this just
// duplicates the Stack Calls section.
func writeGoroutineDump(b *strings.Builder) {
buf := make([]byte, 64*1024)
n := runtime.Stack(buf, true)
if n == len(buf) {
// Grew past buffer — try one larger. Capping at 256K to avoid
// writing megabytes of logs on a runaway goroutine count.
buf = make([]byte, 256*1024)
n = runtime.Stack(buf, true)
}
b.Write(buf[:n])
b.WriteString("\n")
}
func writeErrorSection(b *strings.Builder, oErr hbrt.Value) {
if oErr.IsNil() {
b.WriteString(" (no error object supplied)\n")
return
}
if oErr.IsHash() {
writeHashErr(b, oErr)
return
}
// Unexpected shape — dump raw
fmt.Fprintf(b, " (unexpected error shape: type %s)\n value: %s\n",
typeName(oErr), describe(oErr))
}
// severityLabel maps the numeric ES_* constant to its Harbour name.
// Anything outside the known set falls back to the raw number.
func severityLabel(s int) string {
switch s {
case 0:
return "INFO"
case 1:
return "WARNING"
case 2:
return "ERROR"
case 3:
return "CATASTROPHIC"
}
return "UNKNOWN"
}
// typeName returns a readable type label (mirrors Harbour's ValType plus
// a few Five-specific niceties).
func typeName(v hbrt.Value) string {
switch {
case v.IsNil():
return "NIL"
case v.IsLogical():
return "LOGICAL"
case v.IsNumeric():
return "NUMERIC"
case v.IsString():
return "STRING"
case v.IsDate():
return "DATE"
case v.IsTimestamp():
return "TIMESTAMP"
case v.IsArray():
return "ARRAY"
case v.IsObject():
return "OBJECT"
case v.IsHash():
return "HASH"
case v.IsBlock():
return "BLOCK"
case v.IsPointer():
return "POINTER"
case v.IsSymbol():
return "SYMBOL"
}
return "UNKNOWN"
}
// describe renders a best-effort string form. Strings come back unquoted and
// truncated; everything else falls back to Value.String().
func describe(v hbrt.Value) string {
if v.IsString() {
s := v.AsString()
if len(s) > 200 {
return s[:200] + "…"
}
return s
}
if v.IsNumeric() {
if v.IsInt() {
return fmt.Sprintf("%d", v.AsNumInt())
}
return fmt.Sprintf("%g", v.AsNumDouble())
}
return v.String()
}
func writeHashErr(b *strings.Builder, oErr hbrt.Value) {
h := oErr.AsHash()
getStr := func(key string) string {
if idx := h.Lookup(hbrt.MakeString(key)); idx >= 0 {
return describe(h.Values[idx])
}
return ""
}
getInt := func(key string) int64 {
if idx := h.Lookup(hbrt.MakeString(key)); idx >= 0 {
v := h.Values[idx]
if v.IsNumeric() {
return v.AsNumInt()
}
}
return 0
}
fmt.Fprintf(b, " Description: %s\n", getStr("DESCRIPTION"))
fmt.Fprintf(b, " Operation: %s\n", getStr("OPERATION"))
fmt.Fprintf(b, " SubSystem: %s (GenCode %d, SubCode %d)\n",
getStr("SUBSYSTEM"), getInt("GENCODE"), getInt("SUBCODE"))
fmt.Fprintf(b, " Severity: %s (%d), OsCode: %d\n",
severityLabel(int(getInt("SEVERITY"))), getInt("SEVERITY"), getInt("OSCODE"))
if fn := getStr("FILENAME"); fn != "" {
fmt.Fprintf(b, " FileName: %s\n", fn)
}
// Most recent file/DOS errors — often the *cause* of the Harbour
// error one level up (open failed → field access panic'd).
if lastFErr != 0 || lastDosErr != 0 {
fmt.Fprintf(b, " FError: %d, DosError: %d\n", lastFErr, lastDosErr)
}
if idx := h.Lookup(hbrt.MakeString("ARGS")); idx >= 0 {
args := h.Values[idx]
if args.IsArray() {
arr := args.AsArray()
if len(arr.Items) > 0 {
op := getStr("OPERATION")
// Blanket-redact all args when the operation itself looks
// like a credential-handling call (e.g. "LOGIN", "SIGNIN",
// "AUTHENTICATE") — we can't know which slot held the
// secret so we mask the lot.
mask := isSensitiveKey(op)
fmt.Fprintf(b, " Args (%d):\n", len(arr.Items))
for i, a := range arr.Items {
val := describe(a)
if mask && a.IsString() {
val = "***REDACTED***"
}
fmt.Fprintf(b, " [%d] %s = %s\n", i+1, typeName(a), val)
}
}
}
}
}
func writeWorkareas(b *strings.Builder, t *hbrt.Thread) {
wam, ok := t.WA.(*hbrdd.WorkAreaManager)
if !ok || wam == nil {
b.WriteString(" (no workarea manager)\n")
return
}
count := 0
current := wam.CurrentNum()
wam.EnumerateAreas(func(nWA uint16, alias string, area hbrdd.Area) {
count++
marker := " "
if nWA == current {
marker = "=> "
}
recCount, _ := area.RecCount()
fmt.Fprintf(b, " %s[%3d] %-15s driver=%s rec=%d/%d eof=%v bof=%v del=%v\n",
marker, nWA, alias,
area.Driver().Name(), area.RecNo(), recCount,
area.EOF(), area.BOF(), area.Deleted())
// Open mode — shared/exclusive/readonly — critical for "works on
// my machine" bugs. Optional via type assertion since the Area
// interface doesn't mandate these methods.
type openModer interface {
IsShared() bool
IsReadOnly() bool
}
if om, ok := area.(openModer); ok {
mode := "exclusive"
if om.IsShared() {
mode = "shared"
}
if om.IsReadOnly() {
mode += ", readonly"
}
fmt.Fprintf(b, " mode: %s\n", mode)
}
// Active index: shows which ordering is applied to the area at
// the moment of error. An EOF'd workarea is often actually just
// "filter cut it off" — knowing the tag saves hours.
type orderInfo interface {
CurrentOrder() int
OrderInfo(ordNo int) (*hbrdd.OrderInfo, error)
}
if oi, ok := area.(orderInfo); ok {
if n := oi.CurrentOrder(); n > 0 {
if info, err := oi.OrderInfo(n); err == nil && info != nil {
fmt.Fprintf(b, " order: %s key=%q",
info.Name, info.KeyExpr)
if info.ForExpr != "" {
fmt.Fprintf(b, " for=%q", info.ForExpr)
}
if info.Unique {
b.WriteString(" unique")
}
if info.Descending {
b.WriteString(" desc")
}
b.WriteString("\n")
}
}
}
// Fields
nF := area.FieldCount()
if nF > 0 {
fmt.Fprintf(b, " fields (%d): ", nF)
for i := 0; i < nF && i < 20; i++ {
fi := area.GetFieldInfo(i)
if i > 0 {
b.WriteString(", ")
}
fmt.Fprintf(b, "%s(%c/%d)", fi.Name, fi.Type, fi.Len)
}
if nF > 20 {
fmt.Fprintf(b, ", … %d more", nF-20)
}
b.WriteString("\n")
}
})
if count == 0 {
b.WriteString(" (no open workareas)\n")
}
}
// addCommas formats an int64 with thousands separators so big byte counts
// are legible in the log. No allocation beyond the strings.Builder.
func addCommas(n int64) string {
neg := n < 0
if neg {
n = -n
}
s := fmt.Sprintf("%d", n)
if len(s) <= 3 {
if neg {
return "-" + s
}
return s
}
var out strings.Builder
if neg {
out.WriteByte('-')
}
// Insert commas every 3 digits from the right.
first := len(s) % 3
if first > 0 {
out.WriteString(s[:first])
if len(s) > first {
out.WriteByte(',')
}
}
for i := first; i < len(s); i += 3 {
out.WriteString(s[i : i+3])
if i+3 < len(s) {
out.WriteByte(',')
}
}
return out.String()
}