From a8b6220fae36706cb9e9200d02e94449129aa48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D0=BE=D1=84=D0=B5=D0=B9=20=D0=A8=D0=BA?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=BE=D0=B2?= Date: Thu, 19 Mar 2026 11:20:22 +0300 Subject: [PATCH 1/2] Add runner context interrupting --- main.go | 10 ++++++++++ py/run.go | 8 ++++++++ repl/cli/cli.go | 6 ++++++ repl/repl.go | 12 ++++++++++++ stdlib/stdlib.go | 13 +++++++++++++ vm/eval.go | 10 ++++++++-- 6 files changed, 57 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 8b55ab1e..34d0b740 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,10 @@ import ( "fmt" "log" "os" + "os/signal" "runtime" "runtime/pprof" + "syscall" "github.com/go-python/gpython/py" "github.com/go-python/gpython/repl" @@ -48,6 +50,14 @@ func xmain(args []string) { ctx := py.NewContext(opts) defer ctx.Close() + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + go func() { + for range sigCh { + ctx.SetInterrupt() + } + }() + if *cpuprofile != "" { f, err := os.Create(*cpuprofile) if err != nil { diff --git a/py/run.go b/py/run.go index cd584fc2..7e0e7164 100644 --- a/py/run.go +++ b/py/run.go @@ -42,6 +42,14 @@ type Context interface { // Gereric access to this context's modules / state. Store() *ModuleStore + // SetInterrupt signals the VM to raise KeyboardInterrupt at the next opcode boundary. + // Safe to call from any goroutine (e.g. a signal handler). + SetInterrupt() + + // CheckInterrupt atomically checks and clears the interrupt flag. + // Returns true if an interrupt was pending. + CheckInterrupt() bool + // Close signals this context is about to go out of scope and any internal resources should be released. // Code execution on a py.Context that has been closed will result in an error. Close() error diff --git a/repl/cli/cli.go b/repl/cli/cli.go index f6f2f6c0..a0a6c71f 100644 --- a/repl/cli/cli.go +++ b/repl/cli/cli.go @@ -51,6 +51,7 @@ func newReadline(repl *repl.REPL) *readline { } rl.SetTabCompletionStyle(liner.TabPrints) rl.SetWordCompleter(rl.Completer) + rl.SetCtrlCAborts(true) return rl } @@ -146,6 +147,11 @@ func RunREPL(replCtx *repl.REPL) error { fmt.Printf("\n") break } + if err == liner.ErrPromptAborted { + fmt.Println("KeyboardInterrupt") + rl.repl.ResetContinuation() + continue + } fmt.Printf("Problem reading line: %v\n", err) continue } diff --git a/repl/repl.go b/repl/repl.go index 3938a7b6..3a4d9aed 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -65,6 +65,14 @@ func (r *REPL) SetUI(term UI) { r.term.SetPrompt(NormalPrompt) } +// ResetContinuation cancels any multi-line input in progress, +// restoring the REPL to a clean prompt state (e.g. after Ctrl+C). +func (r *REPL) ResetContinuation() { + r.continuation = false + r.previous = "" + r.term.SetPrompt(NormalPrompt) +} + // Run runs a single line of the REPL func (r *REPL) Run(line string) error { // Override the PrintExpr output temporarily @@ -112,6 +120,10 @@ func (r *REPL) Run(line string) error { if py.IsException(py.SystemExit, err) { return err } + if py.IsException(py.KeyboardInterrupt, err) { + r.term.Print("KeyboardInterrupt") + return nil + } py.TracebackDump(err) } return nil diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 7d1fb811..3ab5d734 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -13,6 +13,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "github.com/go-python/gpython/py" "github.com/go-python/gpython/stdlib/marshal" @@ -44,6 +45,7 @@ type context struct { closed bool running sync.WaitGroup done chan struct{} + interrupt int32 // atomic; non-zero means KeyboardInterrupt pending } // NewContext creates a new gpython interpreter instance context. @@ -192,6 +194,16 @@ func (ctx *context) ResolveAndCompile(pathname string, opts py.CompileOpts) (py. return out, nil } +// See interface py.Context defined in py/run.go +func (ctx *context) SetInterrupt() { + atomic.StoreInt32(&ctx.interrupt, 1) +} + +// See interface py.Context defined in py/run.go +func (ctx *context) CheckInterrupt() bool { + return atomic.SwapInt32(&ctx.interrupt, 0) != 0 +} + func (ctx *context) pushBusy() error { if ctx.closed { return py.ExceptionNewf(py.RuntimeError, "Context closed") @@ -208,6 +220,7 @@ func (ctx *context) popBusy() { func (ctx *context) Close() error { ctx.closeOnce.Do(func() { ctx.closing = true + ctx.SetInterrupt() ctx.running.Wait() ctx.closed = true diff --git a/vm/eval.go b/vm/eval.go index 9db0fae9..a27f82c0 100644 --- a/vm/eval.go +++ b/vm/eval.go @@ -1751,7 +1751,6 @@ func RunFrame(frame *py.Frame) (res py.Object, err error) { frame: frame, context: frame.Context, } - // FIXME need to do this to save the old exeption when we // yield from a generator. Should save it in the Frame though // (see slots in the frame) @@ -1778,6 +1777,13 @@ func RunFrame(frame *py.Frame) (res py.Object, err error) { var arg int32 opcodes := frame.Code.Code for vm.why == whyNot { + // Check for pending interrupt (e.g. SIGINT / Context.SetInterrupt). + // Routed through the normal exception mechanism so that + // try/except/finally blocks are honored. + if vm.context != nil && vm.context.CheckInterrupt() { + vm.SetException(py.MakeException(py.ExceptionNewf(py.KeyboardInterrupt, "KeyboardInterrupt"))) + goto handleException + } if debugging { debugf("* %4d:", frame.Lasti) } @@ -1822,7 +1828,7 @@ func RunFrame(frame *py.Frame) (res py.Object, err error) { if vm.why == whyYield { goto fast_yield } - + handleException: // Something exceptional has happened - unwind the block stack // and find out what for vm.why != whyNot && frame.Block != nil { From 118acd770771d317d255b32fb7bd87e570241004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D0=BE=D1=84=D0=B5=D0=B9=20=D0=A8=D0=BA?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=BE=D0=B2?= Date: Sat, 21 Mar 2026 13:40:10 +0300 Subject: [PATCH 2/2] Remove syscalls and change go version --- go.mod | 2 +- main.go | 3 +-- stdlib/stdlib.go | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c27f93bf..41605b8e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/go-python/gpython -go 1.18 +go 1.25 require ( github.com/google/go-cmp v0.5.8 diff --git a/main.go b/main.go index 34d0b740..96eeda91 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,6 @@ import ( "os/signal" "runtime" "runtime/pprof" - "syscall" "github.com/go-python/gpython/py" "github.com/go-python/gpython/repl" @@ -51,7 +50,7 @@ func xmain(args []string) { defer ctx.Close() sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + signal.Notify(sigCh, os.Interrupt) go func() { for range sigCh { ctx.SetInterrupt() diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 3ab5d734..514cc7c8 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -45,7 +45,7 @@ type context struct { closed bool running sync.WaitGroup done chan struct{} - interrupt int32 // atomic; non-zero means KeyboardInterrupt pending + interrupt atomic.Int32 // non-zero means KeyboardInterrupt pending } // NewContext creates a new gpython interpreter instance context. @@ -196,12 +196,12 @@ func (ctx *context) ResolveAndCompile(pathname string, opts py.CompileOpts) (py. // See interface py.Context defined in py/run.go func (ctx *context) SetInterrupt() { - atomic.StoreInt32(&ctx.interrupt, 1) + ctx.interrupt.Store(1) } // See interface py.Context defined in py/run.go func (ctx *context) CheckInterrupt() bool { - return atomic.SwapInt32(&ctx.interrupt, 0) != 0 + return ctx.interrupt.Swap(0) != 0 } func (ctx *context) pushBusy() error {