feat: better debugger (#13)

- create centralized structured logger with rotation
(https://github.com/versenilvis/IRIS/commit/96ad7c95ef0f7a9957b1bbf70bb6c8da3ce28b2e)
- update core lookup and utils to use new logger
(https://github.com/versenilvis/IRIS/commit/8b74b5a9cdc1828cff8c6d0bdaaccbe31da7a00a)
- add log startup metadata and propagate reload flags
(https://github.com/versenilvis/IRIS/commit/31208aa91a58e9e09abbf5497c314f5c961a58e2)
- add log key interceptions and pty events
(https://github.com/versenilvis/IRIS/commit/91ccb92835dd16ee70bf630fe0f267aad93c9db0)
- add test for logger 221a40c893b75e6215787163cd751a4c1cb6c1d8
- secure reload arguments file path to prevent symlink attacks
(https://github.com/versenilvis/IRIS/pull/13/commits/c3eabfda451230e52cb71f0c2a556687f839bd3f)
- close log file on reinitialization to prevent descriptor leak
(https://github.com/versenilvis/IRIS/pull/13/commits/a57d2c9be107a8c6200cf7046cc85b2f58667dce)
This commit is contained in:
VERSE
2026-07-01 20:09:38 +07:00
committed by GitHub
parent aca3d1670c
commit f37f0681ce
7 changed files with 270 additions and 41 deletions
+13 -18
View File
@@ -8,13 +8,15 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/spf13/cobra"
"github.com/versenilvis/iris/commands/core"
_ "github.com/versenilvis/iris/commands"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/logger"
"golang.org/x/term"
)
@@ -36,6 +38,10 @@ It works exactly like coding editor suggestion menu drop down.`,
}()
if pidStr := os.Getenv("IRIS_PID"); pidStr != "" {
if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 {
if logDir, err := config.CachePath(); err == nil {
argsFile := filepath.Join(logDir, "reload-args")
_ = os.WriteFile(argsFile, []byte(strings.Join(os.Args[1:], "\n")), 0600)
}
_ = syscall.Kill(pid, syscall.SIGUSR1)
fmt.Println("\r\033[K\033[36m[IRIS] Sent reload signal to parent session.\033[0m")
return
@@ -46,7 +52,6 @@ It works exactly like coding editor suggestion menu drop down.`,
}
shellFlag string
debugMode bool
debugLogger *os.File
)
func init() {
@@ -57,26 +62,16 @@ func init() {
if shellFlag != "" {
config.Get().Core.Shell = shellFlag
}
config.Get().Core.Debug = true
if config.Get().Core.Debug {
logDir, err := config.CachePath()
if err == nil {
_ = os.MkdirAll(logDir, 0755)
f, _ := os.OpenFile(filepath.Join(logDir, "iris.log"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
debugLogger = f
core.DebugWriter = f
_, _ = fmt.Fprintf(debugLogger, "--- IRIS DEBUG LOG ---\n")
}
logDir, err := config.CachePath()
if err == nil {
logger.Init(filepath.Join(logDir, "iris.log"), debugMode || config.Get().Core.Debug)
logger.Infof("IRIS session started: os=%s, arch=%s, go=%s, pid=%d", runtime.GOOS, runtime.GOARCH, runtime.Version(), os.Getpid())
cfg := config.Get()
logger.Debugf("IRIS loaded config: shell=%q, mode=%q, ghost-text=%v, max-suggestions=%d", cfg.Core.Shell, cfg.Core.Mode, cfg.UI.GhostText, cfg.UI.MaxSuggestions)
}
}
}
func debugLog(format string, a ...any) {
if debugLogger != nil {
_, _ = fmt.Fprintf(debugLogger, format+"\n", a...)
}
}
// runWatchdog spawns the watchdog parent process
func runWatchdog() {
exe, err := os.Executable()
+3 -3
View File
@@ -7,10 +7,10 @@ import (
"github.com/versenilvis/iris/commands/core"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/integration"
"github.com/versenilvis/iris/logger"
)
// mergeResults collects and dedupes suggestions for a query and mode
// example: mergeResults("git ", "spec")
// MergeResults collects and dedupes suggestions for a query and mode
func MergeResults(query string, mode string) []core.Suggestion {
maxSugg := config.Get().UI.MaxSuggestions
seen := make(map[string]bool)
@@ -19,7 +19,7 @@ func MergeResults(query string, mode string) []core.Suggestion {
// always call lookup to scan aliases and get spec suggestions
var cmdResults []core.Suggestion
if query != "" {
debugLog("[Merge] Calling Lookup for '%s'", query)
logger.Debugf("Merge Calling Lookup for '%s'", query)
cmdResults = core.Lookup(query)
}
+43 -7
View File
@@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"sync"
"sync/atomic"
@@ -20,6 +21,7 @@ import (
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/integration"
"github.com/versenilvis/iris/integration/shell"
"github.com/versenilvis/iris/logger"
"golang.org/x/sys/unix"
"golang.org/x/term"
)
@@ -116,12 +118,16 @@ func runWrapper() {
_ = pty.InheritSize(os.Stdin, ptmx)
core.ShellPID = c.Process.Pid
logger.Infof("PTY child shell started: shell=%s, path=%s, pid=%d", shellName, adapter.GetShellPath(), c.Process.Pid)
// put terminal in raw mode to intercept every keystroke
var errMakeRaw error
oldState, errMakeRaw = term.MakeRaw(int(os.Stdin.Fd()))
if errMakeRaw != nil {
logger.Errorf("Failed to set terminal raw mode: %v", errMakeRaw)
panic(errMakeRaw)
}
logger.Debugf("Terminal set to raw mode successfully")
defer restoreTerminal()
sigCh := make(chan os.Signal, 2)
@@ -139,6 +145,7 @@ func runWrapper() {
for s := range sigCh {
switch s {
case syscall.SIGWINCH:
logger.Debugf("Received SIGWINCH terminal resize signal")
_ = pty.InheritSize(os.Stdin, ptmx) // handle terminal window resize
// this is the core feature of reloading
// it helps IRIS reload itself that you dont need to restart the shell manually
@@ -178,7 +185,25 @@ func runWrapper() {
}
restoreTerminal()
_ = syscall.Exec(exe, os.Args, os.Environ())
execArgs := []string{os.Args[0]}
if logDir, err := config.CachePath(); err == nil {
argsFile := filepath.Join(logDir, "reload-args")
if data, err := os.ReadFile(argsFile); err == nil {
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" {
execArgs = append(execArgs, trimmed)
}
}
_ = os.Remove(argsFile)
} else {
execArgs = os.Args
}
} else {
execArgs = os.Args
}
_ = syscall.Exec(exe, execArgs, os.Environ())
}
}
}()
@@ -305,7 +330,7 @@ func runWrapper() {
writeStdout([]byte(rBuf.String()))
}
if err := scanner.Err(); err != nil {
debugLog("[IPC] scanner error: %v", err)
logger.Errorf("IPC scanner error: %v", err)
}
}()
@@ -347,9 +372,9 @@ func runWrapper() {
writeStdout([]byte(overlay.ClearAndDisable()))
return
}
debugLog("[Render] query: '%s', mode: %s", bufCopy, modeCopy)
logger.Debugf("Render query: '%s', mode: %s", bufCopy, modeCopy)
results := MergeResults(bufCopy, modeCopy)
debugLog("[Render] results found: %d", len(results))
logger.Debugf("Render results found: %d", len(results))
if len(results) == 0 || (len(results) == 1 && strings.TrimSpace(results[0].Cmd) == strings.TrimSpace(bufCopy) && !strings.HasSuffix(bufCopy, " ")) {
b.WriteString(overlay.ClearAndDisable())
@@ -376,7 +401,7 @@ func runWrapper() {
if len(overlay.Items) > 0 && overlay.Cursor >= 0 && overlay.Cursor < len(overlay.Items) {
currentCmd = overlay.Items[overlay.Cursor].Cmd
}
debugLog("[RenderOverlay] nav: %v, cursor: %d, typedQuery: '%s', currentCmd: '%s'", navCopy, overlay.Cursor, overlay.TypedQuery, currentCmd)
logger.Debugf("RenderOverlay nav: %v, cursor: %d, typedQuery: '%s', currentCmd: '%s'", navCopy, overlay.Cursor, overlay.TypedQuery, currentCmd)
b.WriteString(overlay.Render())
writeStdout([]byte(b.String()))
}
@@ -448,6 +473,8 @@ func runWrapper() {
continue
}
logger.Debugf("Stdin raw input: bytes=%q, hex=%x", inputSlice[:n], inputSlice[:n])
shouldOverlayDraw := false
for i := 0; i < n; i++ {
b := inputSlice[i]
@@ -458,6 +485,7 @@ func runWrapper() {
if i+5 < n && inputSlice[i+1] == '[' && inputSlice[i+2] == '2' && inputSlice[i+3] == '0' {
if (inputSlice[i+4] == '0' || inputSlice[i+4] == '1') && inputSlice[i+5] == '~' {
intercepted = true
logger.Debugf("Intercepted bracketed paste event")
_, _ = ptmx.Write(inputSlice[i : i+6])
i += 5
continue
@@ -469,6 +497,7 @@ func runWrapper() {
if inputSlice[i+1] == '[' && inputSlice[i+2] == 'Z' {
intercepted = true
suggestionsEnabled = !suggestionsEnabled
logger.Debugf("Intercepted Shift+Tab, suggestionsEnabled=%v", suggestionsEnabled)
if !suggestionsEnabled {
writeStdout([]byte(overlay.ClearAndDisable()))
} else {
@@ -494,17 +523,20 @@ func runWrapper() {
}
oldCursor := overlay.Cursor
if inputSlice[i+2] == 'A' { // up arrow
arrowDir := "down"
if inputSlice[i+2] == 'A' {
arrowDir = "up"
overlay.Cursor--
if overlay.Cursor < 0 {
overlay.Cursor = 0
}
} else { // down arrow
} else {
overlay.Cursor++
if overlay.Cursor >= len(overlay.Items) {
overlay.Cursor = len(overlay.Items) - 1
}
}
logger.Debugf("Intercepted %s Arrow, cursor moved %d -> %d", arrowDir, oldCursor, overlay.Cursor)
// boundary hit - ignore redundant write to avoid PTY flooding
if overlay.Cursor == oldCursor {
@@ -589,6 +621,7 @@ func runWrapper() {
if hasMatch && len(ghostText) > 0 {
intercepted = true
logger.Debugf("Intercepted Right Arrow (accepted ghost text: %q)", ghostText)
bufferMu.Lock()
naiveBuffer += ghostText
cursorOffset = 0
@@ -681,6 +714,7 @@ func runWrapper() {
}
saveMode(activeMode)
activeModeMu.Unlock()
logger.Debugf("Intercepted Ctrl+R, toggled mode to %q", activeMode)
shouldOverlayDraw = true
// enter: enter behavior is a bit different from tab suggestions in code editor
// I want it to execute the command anyway and ignore the suggestions
@@ -688,6 +722,7 @@ func runWrapper() {
// enter is not used to select suggestions
} else if overlay.Visible && (b == 0x0d || b == 0x0a) {
intercepted = true
logger.Debugf("Intercepted Enter key, navigated=%v", overlay.UserNavigated)
if overlay.UserNavigated && len(overlay.Items) > 0 && overlay.Cursor >= 0 && overlay.Cursor < len(overlay.Items) {
selected := overlay.Items[overlay.Cursor].Cmd
activeModeMu.RLock()
@@ -711,6 +746,7 @@ func runWrapper() {
continue
} else if b == 0x09 { // tab: select suggestions
intercepted = true
logger.Debugf("Intercepted Tab key, visible=%v, cursor=%d", overlay.Visible, overlay.Cursor)
if !overlay.Visible {
shouldOverlayDraw = true
} else {