diff --git a/go.mod b/go.mod index e11435f..89881ac 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.25.0 require ( github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.20.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go/daemon.go b/go/daemon.go index 3983673..0ff6e41 100644 --- a/go/daemon.go +++ b/go/daemon.go @@ -15,6 +15,8 @@ import ( "sync" "sync/atomic" "time" + + "gopkg.in/yaml.v3" ) type daemonState struct { @@ -40,41 +42,15 @@ func handleRequest(state *daemonState, req protocolRequest) response { return response{Output: formatTraceOutput(err, req.TraceLevel)} case "sessions": - sessions := state.manager.list() - if len(sessions) == 0 { + items := state.manager.list() + if len(items) == 0 { return response{Output: "No active sessions."} } - sort.Slice(sessions, func(i, j int) bool { - return sessions[i].lang+sessions[i].label < sessions[j].lang+sessions[j].label - }) - lines := []string{} - for _, s := range sessions { - label := s.label - if strings.HasPrefix(label, "~") { - label = "session " + strings.TrimPrefix(label, "~") - } else { - label = "dir " + label - } - line := "" - line += s.id + " " - line += fmt.Sprintf("[%s] %s", s.lang, label) - if s.project != "" { - line += " project=" + s.project - } - if !s.alive { - line += " status=dead" - } else if s.busyFor > 0 { - line += fmt.Sprintf(" busy=%.1fs", s.busyFor.Seconds()) - } - if len(s.args) > 0 { - line += " args=" + strings.Join(s.args, " ") - } - if s.logFile != "" { - line += " log=" + s.logFile - } - lines = append(lines, line) + out, err := formatSessions(items) + if err != nil { + return errResp(err.Error()) } - return response{Output: strings.Join(lines, "\n")} + return response{Output: out} case "interrupt": key, kerr := state.manager.targetKey(req.ID, req.Lang, req.Session, req.Cwd, discFor(req)) @@ -110,6 +86,24 @@ func handleRequest(state *daemonState, req protocolRequest) response { } } +// formatSessions renders sessions as a YAML sequence with one flow-style +// mapping per line: compact and scannable, yet a single parseable document. +func formatSessions(items []sessionInfo) (string, error) { + sort.Slice(items, func(i, j int) bool { + return items[i].Lang+items[i].Session+items[i].Dir < + items[j].Lang+items[j].Session+items[j].Dir + }) + var root yaml.Node + if err := root.Encode(items); err != nil { + return "", err + } + for _, item := range root.Content { + item.Style = yaml.FlowStyle + } + b, err := yaml.Marshal(&root) + return string(b), err +} + func errResp(msg string) response { return response{Error: msg} } diff --git a/go/engine_test.go b/go/engine_test.go index 4071cbd..bd76687 100644 --- a/go/engine_test.go +++ b/go/engine_test.go @@ -6,9 +6,8 @@ import ( "testing" "time" - "github.com/Beforerr/repld/go/julia" - "github.com/Beforerr/repld/go/python" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestAcceptControl(t *testing.T) { @@ -91,23 +90,40 @@ func TestHandleRequest_SessionsList(t *testing.T) { state := newTestState() // A julia dir session pinned to a project; a global labeled session; a dead // python dir session. The key is lang-prefixed; --session labels are global. - jl := newSession(julia.Adapter{}, "s", []string{"--project=/env"}, nil) - jl.lang = "julia" + jl := newSession("julia", "s", []string{"--project=/env"}, nil) jl.id = "kqzmkqzm" - named := newSession(julia.Adapter{}, "s", nil, nil) - named.lang = "julia" - py := newSession(python.Adapter{}, "s", nil, nil) - py.lang = "python" + named := newSession("julia", "s", nil, nil) + py := newSession("python", "s", nil, nil) py.dead.Store(true) - state.manager.sessions["julia\x00/work\x00/env"] = jl - state.manager.sessions["~scratch"] = named - state.manager.sessions["python\x00/work\x00"] = py + state.manager.sessions[sessionKey{lang: "julia", route: "/work", disc: "/env"}] = jl + state.manager.sessions[sessionKey{label: "scratch"}] = named + state.manager.sessions[sessionKey{lang: "python", route: "/work"}] = py resp := handleRequest(state, protocolRequest{Action: "sessions"}) require.Empty(t, resp.Error) - require.Equal(t, `kqzmkqzm [julia] dir /work project=/env args=--project=/env - [julia] session scratch project=@. - [python] dir /work status=dead`, resp.Output) + require.Equal(t, `- {id: kqzmkqzm, lang: julia, dir: /work, args: [--project=/env]} +- {lang: julia, session: scratch, args: [--project=@.]} +- {lang: python, dir: /work, status: dead} +`, resp.Output) +} + +func TestHandleRequest_SessionsParseableYAML(t *testing.T) { + state := newTestState() + jl := newSession("julia", "s", []string{"--project=/env"}, nil) + jl.id = "kqzmkqzm" + named := newSession("julia", "s", nil, nil) + state.manager.sessions[sessionKey{lang: "julia", route: "/work", disc: "/env"}] = jl + state.manager.sessions[sessionKey{label: "scratch"}] = named + + resp := handleRequest(state, protocolRequest{Action: "sessions"}) + require.Empty(t, resp.Error) + + var items []sessionInfo + require.NoError(t, yaml.Unmarshal([]byte(resp.Output), &items)) + require.Len(t, items, 2) + require.Equal(t, "kqzmkqzm", items[0].ID) + require.Equal(t, "julia", items[0].Lang) + require.Equal(t, "scratch", items[1].Session) } func TestInterruptUnknownSession(t *testing.T) { @@ -124,9 +140,8 @@ func TestCloseUnknownSession(t *testing.T) { func TestCloseSession(t *testing.T) { state := newTestState() - sess := newSession(julia.Adapter{}, "s", nil, nil) - sess.lang = "julia" - state.manager.sessions["~scratch"] = sess + sess := newSession("julia", "s", nil, nil) + state.manager.sessions[sessionKey{label: "scratch"}] = sess resp := handleRequest(state, protocolRequest{Action: "close", Session: "scratch", Cwd: t.TempDir()}) require.Empty(t, resp.Error) @@ -137,10 +152,9 @@ func TestCloseSession(t *testing.T) { func TestCloseSessionByID(t *testing.T) { state := newTestState() - sess := newSession(julia.Adapter{}, "s", nil, nil) - sess.lang = "julia" + sess := newSession("julia", "s", nil, nil) sess.id = "kqzm" - state.manager.sessions["julia\x00/work\x00@."] = sess + state.manager.sessions[sessionKey{lang: "julia", route: "/work", disc: "@."}] = sess // Unique prefix resolves regardless of cwd. resp := handleRequest(state, protocolRequest{Action: "close", ID: "kq", Cwd: t.TempDir()}) @@ -155,24 +169,25 @@ func TestCloseSessionByID(t *testing.T) { func TestKeyForIDPrefix(t *testing.T) { m := newSessionManager() defer m.shutdown() - a := newSession(julia.Adapter{}, "s", nil, nil) + a := newSession("julia", "s", nil, nil) a.id = "kqzm" - b := newSession(julia.Adapter{}, "s", nil, nil) + b := newSession("julia", "s", nil, nil) b.id = "kxyz" - m.sessions["julia\x00/a\x00@."] = a - m.sessions["julia\x00/b\x00@."] = b + m.sessions[sessionKey{lang: "julia", route: "/a", disc: "@."}] = a + m.sessions[sessionKey{lang: "julia", route: "/b", disc: "@."}] = b - key, err := m.keyForID("kq") + key, ok, err := m.keyForID("kq") require.NoError(t, err) - require.Equal(t, "julia\x00/a\x00@.", key) + require.True(t, ok) + require.Equal(t, sessionKey{lang: "julia", route: "/a", disc: "@."}, key) - _, err = m.keyForID("k") // shared prefix → ambiguous + _, _, err = m.keyForID("k") // shared prefix → ambiguous require.Error(t, err) require.Contains(t, err.Error(), "ambiguous") - key, err = m.keyForID("zz") // no match + _, ok, err = m.keyForID("zz") // no match require.NoError(t, err) - require.Equal(t, "", key) + require.False(t, ok) } func TestSessionManagerKey(t *testing.T) { @@ -180,8 +195,8 @@ func TestSessionManagerKey(t *testing.T) { defer m.shutdown() // key = lang + cwd + discriminant (project); label keys are global. - require.Equal(t, "julia\x00/w\x00@.", m.key("julia", "", "/w", "@.")) - require.Equal(t, "python\x00/w\x00", m.key("python", "", "/w", "")) + require.Equal(t, sessionKey{lang: "julia", route: "/w", disc: "@."}, m.key("julia", "", "/w", "@.")) + require.Equal(t, sessionKey{lang: "python", route: "/w"}, m.key("python", "", "/w", "")) // same dir, distinct by language or by project → distinct sessions. require.NotEqual(t, m.key("julia", "", "/w", "@."), m.key("python", "", "/w", "")) require.NotEqual(t, m.key("julia", "", "/w", "@."), m.key("julia", "", "/w", "/env")) @@ -189,6 +204,6 @@ func TestSessionManagerKey(t *testing.T) { require.Equal(t, m.key("julia", "", "/a", absProject), m.key("julia", "", "/b", absProject)) require.NotEqual(t, m.key("julia", "", "/a", "@."), m.key("julia", "", "/b", "@.")) // a --session label is global: same key regardless of language/project. - require.Equal(t, "~scratch", m.key("julia", "scratch", "/w", "@.")) - require.Equal(t, "~scratch", m.key("python", "scratch", "/other", "")) -} + require.Equal(t, sessionKey{label: "scratch"}, m.key("julia", "scratch", "/w", "@.")) + require.Equal(t, sessionKey{label: "scratch"}, m.key("python", "scratch", "/other", "")) +} \ No newline at end of file diff --git a/go/integration_test.go b/go/integration_test.go index 0c9a03b..bcccbcd 100644 --- a/go/integration_test.go +++ b/go/integration_test.go @@ -14,7 +14,6 @@ import ( "testing" "time" - "github.com/Beforerr/repld/go/julia" "github.com/stretchr/testify/require" ) @@ -323,7 +322,7 @@ func TestCLINoInterpreterDoesNotCreateMissingNamedSession(t *testing.T) { } func TestExecuteRawWithoutControlDoesNotPanic(t *testing.T) { - sess := newSession(julia.Adapter{}, "SENTINEL", nil, nil) + sess := newSession("julia", "SENTINEL", nil, nil) sess.stdin = &nopWriteCloser{} sess.stdout = bufio.NewReader(strings.NewReader("SENTINEL\n")) sess.stderr = bufio.NewReader(strings.NewReader("SENTINEL\n")) diff --git a/go/julia_integration_test.go b/go/julia_integration_test.go index 53fa0ad..99e75a7 100644 --- a/go/julia_integration_test.go +++ b/go/julia_integration_test.go @@ -235,12 +235,12 @@ func TestInterruptBusyAndUnblocks(t *testing.T) { var sessOut string for time.Now().Before(deadline) { sessOut = repldOK(t, socketPath, cwd, "sessions").stdout - if strings.Contains(sessOut, "busy=") { + if strings.Contains(sessOut, "status: busy") { break } time.Sleep(50 * time.Millisecond) } - require.Contains(t, sessOut, "busy=", "sessions listing should show busy= while a call is in flight") + require.Contains(t, sessOut, "status: busy", "sessions listing should show status: busy while a call is in flight") irq := repldOK(t, socketPath, cwd, "interrupt", "julia") require.Contains(t, irq.stdout, "interrupted", "sleep must survive the interrupt, got: %q", irq.stdout) @@ -289,7 +289,7 @@ func TestClientDisconnectInterruptsEval(t *testing.T) { // Wait until the session reports busy. deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { - if strings.Contains(sendRequest(t, socketPath, protocolRequest{Action: "sessions"}).Output, "busy=") { + if strings.Contains(sendRequest(t, socketPath, protocolRequest{Action: "sessions"}).Output, "status: busy") { break } time.Sleep(50 * time.Millisecond) @@ -301,7 +301,7 @@ func TestClientDisconnectInterruptsEval(t *testing.T) { // The session must stop being busy promptly. Without disconnect handling, // the orphaned sleep(60) would keep the session busy for ~60s. require.Eventually(t, func() bool { - return !strings.Contains(sendRequest(t, socketPath, protocolRequest{Action: "sessions"}).Output, "busy=") + return !strings.Contains(sendRequest(t, socketPath, protocolRequest{Action: "sessions"}).Output, "status: busy") }, 10*time.Second, 100*time.Millisecond, "client disconnect should have interrupted the orphaned eval") // Session must be usable again AND have survived with state intact @@ -381,7 +381,7 @@ func TestJuliaWorldAgeDisplay(t *testing.T) { func TestKillRunsAtexitHooks(t *testing.T) { cwd, err := os.Getwd() require.NoError(t, err) - sess := newSession(julia.Adapter{}, newSentinel(), nil, nil) + sess := newSession("julia", newSentinel(), nil, nil) require.NoError(t, sess.start("", cwd)) marker := filepath.Join(t.TempDir(), "atexit.marker") @@ -431,7 +431,7 @@ func TestQueuedDisconnectDoesNotInterruptRunningEval(t *testing.T) { })) require.Eventually(t, func() bool { - return strings.Contains(sendRequest(t, socketPath, protocolRequest{Action: "sessions"}).Output, "busy=") + return strings.Contains(sendRequest(t, socketPath, protocolRequest{Action: "sessions"}).Output, "status: busy") }, 5*time.Second, 50*time.Millisecond, "conn1 eval should be running") // conn2: same session, queues behind conn1, then disconnects abruptly. diff --git a/go/main.go b/go/main.go index 3cae36d..cdc7ec7 100644 --- a/go/main.go +++ b/go/main.go @@ -239,7 +239,7 @@ repld flags: Commands (trace/interrupt/close locate a session by [exe], --session, or the short id shown by 'sessions'; an id prefix works when unambiguous): - sessions List active sessions (all languages) + sessions List active sessions (one per line) trace Print the last saved error traceback for the session interrupt Interrupt the in-flight eval (SIGKILL after 3s if unresponsive) close Kill the session's interpreter and discard its state diff --git a/go/manager.go b/go/manager.go index f8920a4..7e8fa4b 100644 --- a/go/manager.go +++ b/go/manager.go @@ -14,27 +14,63 @@ import ( type SessionManager struct { mu sync.Mutex - sessions map[string]*Session - lastErrors map[string]*evalError + sessions map[sessionKey]*Session + lastErrors map[sessionKey]*evalError sf singleflight.Group logDir string } +type sessionKey struct { + label string + lang string + route string + disc string +} + +func (k sessionKey) String() string { + if k.label != "" { + return "session:" + k.label + } + if k.disc == "" { + return k.lang + ":" + k.route + } + return k.lang + ":" + k.route + ":" + k.disc +} + +func (k sessionKey) logName() string { + if k.label != "" { + return "session-" + safeLogComponent(k.label) + } + parts := []string{safeLogComponent(k.lang), safeLogComponent(k.route)} + if k.disc != "" { + parts = append(parts, safeLogComponent(k.disc)) + } + return strings.Join(parts, "-") +} + +func safeLogComponent(s string) string { + s = strings.NewReplacer("/", "_", "\\", "_", ":", "_", "\"", "_").Replace(strings.Trim(s, "/")) + if s == "" { + return "default" + } + return s +} + func newSessionManager() *SessionManager { logDir, _ := os.MkdirTemp("", "repld-logs-") return &SessionManager{ - sessions: make(map[string]*Session), - lastErrors: make(map[string]*evalError), + sessions: make(map[sessionKey]*Session), + lastErrors: make(map[sessionKey]*evalError), logDir: logDir, } } // key namespaces a session. A --session label is global (reusable without -// re-specifying interpreter). Otherwise it's lang + cwd + disc, where disc -// is the adapter's per-environment discriminant (Julia's --project). -func (m *SessionManager) key(lang, session, cwd, disc string) string { +// re-specifying interpreter). Otherwise route is cwd or the adapter's +// per-environment location (Julia's --project). +func (m *SessionManager) key(lang, session, cwd, disc string) sessionKey { if session != "" { - return "~" + session + return sessionKey{label: session} } route := cwd if lang == "julia" && disc != "" && disc != "@." { @@ -44,7 +80,7 @@ func (m *SessionManager) key(lang, session, cwd, disc string) string { } route = filepath.Clean(route) } - return lang + "\x00" + route + "\x00" + disc + return sessionKey{lang: lang, route: route, disc: disc} } // from k-z : IDs never look like exe names, paths @@ -72,50 +108,39 @@ func (m *SessionManager) uniqueIDLocked() string { } } -func (m *SessionManager) keyForID(prefix string) (string, error) { +func (m *SessionManager) keyForID(prefix string) (sessionKey, bool, error) { m.mu.Lock() defer m.mu.Unlock() - match := "" + var match sessionKey + found := false for key, sess := range m.sessions { if sess.id != "" && strings.HasPrefix(sess.id, prefix) { - if match != "" { - return "", fmt.Errorf("session id %q is ambiguous", prefix) + if found { + return sessionKey{}, false, fmt.Errorf("session id %q is ambiguous", prefix) } match = key + found = true } } - return match, nil + return match, found, nil } -func (m *SessionManager) targetKey(id, lang, session, cwd, disc string) (string, error) { +func (m *SessionManager) targetKey(id, lang, session, cwd, disc string) (sessionKey, error) { if id != "" { - key, err := m.keyForID(id) + key, ok, err := m.keyForID(id) if err != nil { - return "", err + return sessionKey{}, err } - if key == "" { - return "", fmt.Errorf("no session with id %q", id) + if !ok { + return sessionKey{}, fmt.Errorf("no session with id %q", id) } return key, nil } return m.key(lang, session, cwd, disc), nil } -// keyLabel is the human label for a key: a "~label" session label, else the cwd -// (the lang prefix and project discriminant are shown separately). -func keyLabel(key string) string { - if !strings.Contains(key, "\x00") { - return key - } - return strings.SplitN(key, "\x00", 3)[1] -} - -func (m *SessionManager) openLogFile(key string) *os.File { - safe := strings.NewReplacer("/", "_", "\\", "_", "\x00", "-").Replace(strings.Trim(key, "/~")) - if safe == "" { - safe = "default" - } - f, _ := os.OpenFile(filepath.Join(m.logDir, safe+".log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) +func (m *SessionManager) openLogFile(key sessionKey) *os.File { + f, _ := os.OpenFile(filepath.Join(m.logDir, key.logName()+".log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) return f } @@ -136,7 +161,7 @@ func (m *SessionManager) getOrCreate(lang, cwd, session, exe string, fwd []strin return sess, nil } - v, err, _ := m.sf.Do(key, func() (any, error) { + v, err, _ := m.sf.Do(key.String(), func() (any, error) { m.mu.Lock() sess := m.sessions[key] m.mu.Unlock() @@ -153,8 +178,7 @@ func (m *SessionManager) getOrCreate(lang, cwd, session, exe string, fwd []strin m.mu.Unlock() } - sess = newSession(lc.adapter, newSentinel(), fwd, m.openLogFile(key)) - sess.lang = lang + sess = newSession(lang, newSentinel(), fwd, m.openLogFile(key)) if err := sess.start(exe, cwd); err != nil { return nil, err } @@ -190,7 +214,7 @@ func (m *SessionManager) restart(lang, session, cwd, disc string) { } } -func (m *SessionManager) close(key string) (string, error) { +func (m *SessionManager) close(key sessionKey) (string, error) { m.mu.Lock() sess := m.sessions[key] delete(m.sessions, key) @@ -218,21 +242,31 @@ func (m *SessionManager) recordError(lang, session, cwd, disc string, err *evalE m.mu.Unlock() } -func (m *SessionManager) lastError(key string) *evalError { +func (m *SessionManager) lastError(key sessionKey) *evalError { m.mu.Lock() defer m.mu.Unlock() return m.lastErrors[key] } +// sessionInfo is both the internal listing row and the YAML output shape. type sessionInfo struct { - id string - lang string - label string // session label or cwd - project string // environment discriminant (Julia --project); "" if none - alive bool - args []string - logFile string - busyFor time.Duration // 0 when idle + ID string `yaml:"id,omitempty"` + Lang string `yaml:"lang"` + Dir string `yaml:"dir,omitempty"` // working directory; "" for named sessions + Session string `yaml:"session,omitempty"` // named-session label; "" for cwd-keyed sessions + Status string `yaml:"status,omitempty"` // "dead" or "busy"; "" when idle and alive + Busy float64 `yaml:"busy,omitempty"` // seconds in-flight; set only while busy + Args []string `yaml:"args,omitempty"` // effective launch args (Julia's implicit --project is made explicit) + Log string `yaml:"log,omitempty"` +} + +func hasProjectFlag(args []string) bool { + for _, a := range args { + if a == "--project" || strings.HasPrefix(a, "--project=") { + return true + } + } + return false } func (m *SessionManager) list() []sessionInfo { @@ -242,27 +276,34 @@ func (m *SessionManager) list() []sessionInfo { result := make([]sessionInfo, 0, len(m.sessions)) for key, sess := range m.sessions { info := sessionInfo{ - id: sess.id, - lang: sess.lang, - label: keyLabel(key), - alive: sess.isAlive(), - args: sess.fwd, + ID: sess.id, + Lang: sess.lang, + Args: sess.fwd, + } + if key.label != "" { + info.Session = key.label + } else { + info.Dir = key.route } - if sess.lang == "julia" { - info.project = sess.adapter.SessionKey("", sess.fwd) + // Surface Julia's implicit project so args reflect the real launch. + if sess.lang == "julia" && !hasProjectFlag(sess.fwd) { + info.Args = append([]string{"--project=" + sess.adapter.SessionKey("", sess.fwd)}, sess.fwd...) } - if since := sess.busySince.Load(); since != 0 { - info.busyFor = now.Sub(time.Unix(0, since)) + if !sess.isAlive() { + info.Status = "dead" + } else if since := sess.busySince.Load(); since != 0 { + info.Status = "busy" + info.Busy = now.Sub(time.Unix(0, since)).Seconds() } if sess.logFile != nil { - info.logFile = sess.logFile.Name() + info.Log = sess.logFile.Name() } result = append(result, info) } return result } -func (m *SessionManager) interrupt(key string, graceSecs float64) (string, error) { +func (m *SessionManager) interrupt(key sessionKey, graceSecs float64) (string, error) { m.mu.Lock() sess := m.sessions[key] m.mu.Unlock() @@ -291,7 +332,7 @@ func (m *SessionManager) shutdown() { for _, s := range m.sessions { sessions = append(sessions, s) } - m.sessions = make(map[string]*Session) + m.sessions = make(map[sessionKey]*Session) m.mu.Unlock() for _, s := range sessions { diff --git a/go/parse_test.go b/go/parse_test.go index eb8c842..5c38e22 100644 --- a/go/parse_test.go +++ b/go/parse_test.go @@ -168,10 +168,6 @@ func TestParseArgs(t *testing.T) { {"missing file forwards after launch flags", []string{"julia", "--project", "/env", "script.jl", "arg1"}, parsed{exe: "julia", fwd: []string{"--project", "/env", "script.jl", "arg1"}}}, {"subcommand", []string{"sessions"}, parsed{sub: "sessions"}}, - {"flags before subcommand", []string{"--socket", "x", "sessions"}, - parsed{socket: "x", sub: "sessions"}}, - {"bare tokens forward once forwarding starts", []string{"-L", "a.jl", "sessions"}, - parsed{fwd: []string{"-L", "a.jl", "sessions"}}}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/go/r_integration_test.go b/go/r_integration_test.go index 1c7e007..fc86301 100644 --- a/go/r_integration_test.go +++ b/go/r_integration_test.go @@ -98,12 +98,12 @@ func TestRInterruptSurvives(t *testing.T) { var sessOut string for time.Now().Before(deadline) { sessOut = repldOK(t, socketPath, cwd, "sessions").stdout - if strings.Contains(sessOut, "busy=") { + if strings.Contains(sessOut, "status: busy") { break } time.Sleep(50 * time.Millisecond) } - require.Contains(t, sessOut, "busy=", "sessions listing should show busy= while a call is in flight") + require.Contains(t, sessOut, "status: busy", "sessions listing should show status: busy while a call is in flight") irq := repldOK(t, socketPath, cwd, "interrupt", "--session", "rirq") require.Contains(t, irq.stdout, "interrupted", "R must survive SIGINT, got: %q", irq.stdout) diff --git a/go/session.go b/go/session.go index 346d674..f46b4ec 100644 --- a/go/session.go +++ b/go/session.go @@ -56,9 +56,10 @@ func newSentinel() string { return fmt.Sprintf("__REPLD_%s__", hex.EncodeToString(b)) } -func newSession(adapter Adapter, sentinel string, fwd []string, logFile *os.File) *Session { +func newSession(lang, sentinel string, fwd []string, logFile *os.File) *Session { return &Session{ - adapter: adapter, + adapter: adapterFor(lang), + lang: lang, sentinel: sentinel, fwd: fwd, logFile: logFile,