Skip to content

Commit

Permalink
Handle sessions and store_last_stdout in runnerv2service
Browse files Browse the repository at this point in the history
  • Loading branch information
adambabik committed Feb 2, 2024
1 parent 6576b3e commit e89e2a6
Show file tree
Hide file tree
Showing 24 changed files with 535 additions and 388 deletions.
19 changes: 3 additions & 16 deletions internal/api/runme/runner/v2alpha1/runner.proto
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,6 @@ message ExecuteRequest {
// has no effect in non-interactive mode
optional Winsize winsize = 10;

// background, if true, will send process' PID as a first response.
bool background = 11;

// session_id indicates in which Session the program should execute.
// Executing in a Session might provide additional context like
// environment variables.
Expand All @@ -121,19 +118,9 @@ message ExecuteRequest {
// project used to load environment variables from .env files.
optional Project project = 22;

// store_last_output, if true, will store the stdout of
// store_last_stdout, if true, will store the stdout of
// the last ran block in the environment variable `__`.
bool store_last_output = 23;

// language_id indicates a language to exeucute scripts/commands.
string language_id = 25;

// file_extension is associated with the script.
string file_extension = 26;
}

message ProcessPID {
int64 pid = 1;
bool store_last_stdout = 23;
}

message ExecuteResponse {
Expand All @@ -149,7 +136,7 @@ message ExecuteResponse {
// pid contains the process' PID.
// This is only sent once in an initial response
// for background processes.
ProcessPID pid = 4;
google.protobuf.UInt32Value pid = 4;
}

service RunnerService {
Expand Down
6 changes: 4 additions & 2 deletions internal/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ The kernel is used to run long running processes like shells and interacting wit

logger.Info("started listening", zap.String("addr", lis.Addr().String()))

const maxMsgSize = 4 * 1024 * 1024 // 4 MiB

server := grpc.NewServer(
grpc.MaxRecvMsgSize(runner.MaxMsgSize),
grpc.MaxSendMsgSize(runner.MaxMsgSize),
grpc.MaxRecvMsgSize(maxMsgSize),
grpc.MaxSendMsgSize(maxMsgSize),
)
parserv1.RegisterParserServiceServer(server, editorservice.NewParserServiceServer(logger))
projectv1.RegisterProjectServiceServer(server, projectservice.NewProjectServiceServer(logger))
Expand Down
8 changes: 4 additions & 4 deletions internal/command/command_exec_normalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ const (
envEndFileName = ".env_end"
)

// envDumpCommand is a command that dumps the environment variables.
// EnvDumpCommand is a command that dumps the environment variables.
// It is declared as a var, because it must be replaced in tests.
// Equivalent is `env -0`.
var envDumpCommand = func() string {
var EnvDumpCommand = func() string {
path, err := os.Executable()
if err != nil {
panic(errors.WithMessage(err, "failed to get the executable path"))
Expand Down Expand Up @@ -59,7 +59,7 @@ func (n *argsNormalizer) Normalize(cfg *Config) (*Config, error) {
if err := n.createTempDir(); err != nil {
return nil, err
}
_, _ = buf.WriteString(fmt.Sprintf("%s > %s\n", envDumpCommand, filepath.Join(n.tempDir, envStartFileName)))
_, _ = buf.WriteString(fmt.Sprintf("%s > %s\n", EnvDumpCommand, filepath.Join(n.tempDir, envStartFileName)))
}
}

Expand All @@ -74,7 +74,7 @@ func (n *argsNormalizer) Normalize(cfg *Config) (*Config, error) {

if isShellLanguage(filepath.Base(cfg.ProgramName)) {
if n.session != nil {
_, _ = buf.WriteString(fmt.Sprintf("%s > %s\n", envDumpCommand, filepath.Join(n.tempDir, envEndFileName)))
_, _ = buf.WriteString(fmt.Sprintf("%s > %s\n", EnvDumpCommand, filepath.Join(n.tempDir, envEndFileName)))

n.isEnvCollectable = true
}
Expand Down
11 changes: 0 additions & 11 deletions internal/command/command_native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,6 @@ func TestNativeCommand(t *testing.T) {
assert.Equal(t, "test", stdout.String())
})

t.Run("WithSession", func(t *testing.T) {
sess := NewSession()
opts := &NativeCommandOptions{
Session: sess,
}
cmd, err := NewNative(testConfigShellProgram, opts)
require.NoError(t, err)
require.NoError(t, cmd.Start(context.Background()))
require.NoError(t, cmd.Wait())
})

t.Run("DevStdin", func(t *testing.T) {
cmd, err := NewNative(testConfigShellProgram, &NativeCommandOptions{Stdin: os.Stdin})
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion internal/command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var (
)

func init() {
envDumpCommand = "env -0"
EnvDumpCommand = "env -0"
}

func TestExecutionCommandFromCodeBlocks(t *testing.T) {
Expand Down
23 changes: 10 additions & 13 deletions internal/command/command_virtual.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,6 @@ func (c *VirtualCommand) Pid() int {
return c.cmd.Process.Pid
}

func (c *VirtualCommand) SetWinsize(rows, cols, x, y uint16) error {
if c.pty == nil {
return nil
}
err := pty.Setsize(c.pty, &pty.Winsize{
Rows: rows,
Cols: cols,
X: x,
Y: y,
})
return errors.WithStack(err)
}

func (c *VirtualCommand) Start(ctx context.Context) (err error) {
argsNormalizer := &argsNormalizer{
session: c.opts.Session,
Expand Down Expand Up @@ -280,6 +267,16 @@ func (c *VirtualCommand) cleanup() {
}
}

type Winsize pty.Winsize

func SetWinsize(cmd *VirtualCommand, winsize *Winsize) error {
if cmd.pty == nil {
return nil
}
err := pty.Setsize(cmd.pty, (*pty.Winsize)(winsize))
return errors.WithStack(err)
}

func isNil(val any) bool {
if val == nil {
return true
Expand Down
2 changes: 1 addition & 1 deletion internal/command/command_virtual_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestVirtualCommand(t *testing.T) {
)
require.NoError(t, err)
require.NoError(t, cmd.Start(context.Background()))
require.NoError(t, cmd.SetWinsize(24, 80, 0, 0))
require.NoError(t, SetWinsize(cmd, &Winsize{Rows: 24, Cols: 80, X: 0, Y: 0}))
require.NoError(t, cmd.Wait())
require.Equal(t, "80\r\n24\r\n", stdout.String())
})
Expand Down
33 changes: 14 additions & 19 deletions internal/command/env_store.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
package command

import (
"errors"
"strings"
"sync"

"golang.org/x/exp/slices"
)

// maxEnvironSize is a maximum limit of size of all env name and value pairs,
// including equal sign and NUL in each pair.
const maxEnvironSize = 32760

func envPairSize(k, v string) int {
// +2 for the '=' and '\0' separators
return len(k) + len(v) + 2
}
// func envPairSize(k, v string) int {
// // +2 for the '=' and '\0' separators
// return len(k) + len(v) + 2
// }

type envStore struct {
mu sync.RWMutex
Expand All @@ -39,18 +34,18 @@ func (s *envStore) Set(k, v string) (*envStore, error) {
s.mu.Lock()
defer s.mu.Unlock()

environSize := envPairSize(k, v)
// environSize := envPairSize(k, v)

for key, value := range s.items {
if key == k {
continue
}
environSize += envPairSize(key, value)
}
// for key, value := range s.items {
// if key == k {
// continue
// }
// environSize += envPairSize(key, value)
// }

if environSize > maxEnvironSize {
return s, errors.New("could not set environment variable, environment size limit exceeded")
}
// if environSize > MaxEnvironSizInBytes {
// return s, errors.New("could not set environment variable, environment size limit exceeded")
// }

s.items[k] = v

Expand Down
9 changes: 9 additions & 0 deletions internal/command/env_store_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !windows

package command

// MaxEnvironSizInBytes is the maximum size of an environment variable
// including equal sign and NUL separators.
//
// This size is an artificial limit as Linux and macOS do not have a real limit.
const MaxEnvironSizInBytes = 128 * 1000 * 1000 // 128 MB
7 changes: 7 additions & 0 deletions internal/command/env_store_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build windows

package command

// MaxEnvironSizInBytes is the maximum size of an environment variable
// including equal sign and NUL separators.
const MaxEnvironSizInBytes = 32767
66 changes: 66 additions & 0 deletions internal/command/session.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package command

import (
"sync"

lru "github.com/hashicorp/golang-lru/v2"

"github.com/stateful/runme/internal/ulid"
)

// Session is an object which lifespan contains multiple executions.
// It's used to exchange information between executions. Currently,
// it only keeps track of environment variables.
type Session struct {
ID string
envStore *envStore
}

func NewSession() *Session {
return &Session{
ID: ulid.GenerateID(),
envStore: newEnvStore(),
}
}
Expand Down Expand Up @@ -38,3 +48,59 @@ func (s *Session) GetEnv() []string {
}
return s.envStore.Items()
}

// SessionListCapacity is a maximum number of sessions
// stored in a single SessionList.
const SessionListCapacity = 1024

type SessionList struct {
items *lru.Cache[string, *Session]
// Even though, the lry.Cache is thread-safe, it does not provide a way to
// get the most recent added session.
mu sync.Mutex
latestSession *Session
}

func NewSessionList() (*SessionList, error) {
cache, err := lru.New[string, *Session](SessionListCapacity)
if err != nil {
return nil, err
}
return &SessionList{items: cache}, nil
}

func (sl *SessionList) Add(session *Session) {
sl.items.Add(session.ID, session)

sl.mu.Lock()
sl.latestSession = session
sl.mu.Unlock()
}

func (sl *SessionList) Get(id string) (*Session, bool) {
return sl.items.Get(id)
}

func (sl *SessionList) List() []*Session {
return sl.items.Values()
}

func (sl *SessionList) Delete(id string) bool {
ok := sl.items.Remove(id)
if !ok {
return ok
}

sl.mu.Lock()
if sl.latestSession != nil && sl.latestSession.ID == id {
keys := sl.items.Keys()
sl.latestSession, _ = sl.items.Get(keys[len(keys)-1])
}
sl.mu.Unlock()

return ok
}

func (sl *SessionList) Newest() (*Session, bool) {
return sl.latestSession, sl.latestSession != nil
}
1 change: 1 addition & 0 deletions internal/gen/proto/go/runme/runner/v2alpha1/config.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e89e2a6

Please sign in to comment.