Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work on windows I think.

We probably only need os.Interrupt anyway, INT and TERM are not generated by the keyboard.

Perhaps see what python3 does with those signals?

Copy link
Author

@TOMMy-Net TOMMy-Net Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"INT and TERM are not generated by the keyboard." You sure? I thought that INT was called by pressing CTRL+C.

go func() {
for range sigCh {
ctx.SetInterrupt()
}
}()

if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
Expand Down
8 changes: 8 additions & 0 deletions py/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions repl/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func newReadline(repl *repl.REPL) *readline {
}
rl.SetTabCompletionStyle(liner.TabPrints)
rl.SetWordCompleter(rl.Completer)
rl.SetCtrlCAborts(true)
return rl
}

Expand Down Expand Up @@ -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
}
Expand Down
12 changes: 12 additions & 0 deletions repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions stdlib/stdlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"path/filepath"
"strings"
"sync"
"sync/atomic"

"github.com/go-python/gpython/py"
"github.com/go-python/gpython/stdlib/marshal"
Expand Down Expand Up @@ -44,6 +45,7 @@ type context struct {
closed bool
running sync.WaitGroup
done chan struct{}
interrupt int32 // atomic; non-zero means KeyboardInterrupt pending
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using atomic.Int32 here saves mistakes

Copy link
Author

@TOMMy-Net TOMMy-Net Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go 1.8, not support this type of atomics @ncw

}

// NewContext creates a new gpython interpreter instance context.
Expand Down Expand Up @@ -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")
Expand All @@ -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

Expand Down
10 changes: 8 additions & 2 deletions vm/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading