Skip to content

Commit d10a605

Browse files
committed
feat: workspace bash background parameter
1 parent f641d85 commit d10a605

File tree

3 files changed

+62
-9
lines changed

3 files changed

+62
-9
lines changed

codersdk/toolsdk/bash.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package toolsdk
33
import (
44
"bytes"
55
"context"
6+
_ "embed"
7+
"encoding/base64"
68
"errors"
79
"fmt"
810
"io"
@@ -18,19 +20,24 @@ import (
1820
"github.com/coder/coder/v2/cli/cliui"
1921
"github.com/coder/coder/v2/codersdk"
2022
"github.com/coder/coder/v2/codersdk/workspacesdk"
23+
"github.com/coder/coder/v2/cryptorand"
2124
)
2225

2326
type WorkspaceBashArgs struct {
24-
Workspace string `json:"workspace"`
25-
Command string `json:"command"`
26-
TimeoutMs int `json:"timeout_ms,omitempty"`
27+
Workspace string `json:"workspace"`
28+
Command string `json:"command"`
29+
TimeoutMs int `json:"timeout_ms,omitempty"`
30+
Background bool `json:"background,omitempty"`
2731
}
2832

2933
type WorkspaceBashResult struct {
3034
Output string `json:"output"`
3135
ExitCode int `json:"exit_code"`
3236
}
3337

38+
//go:embed resources/background.sh
39+
var backgroundScript string
40+
3441
var WorkspaceBash = Tool[WorkspaceBashArgs, WorkspaceBashResult]{
3542
Tool: aisdk.Tool{
3643
Name: ToolNameWorkspaceBash,
@@ -53,6 +60,7 @@ If the command times out, all output captured up to that point is returned with
5360
Examples:
5461
- workspace: "my-workspace", command: "ls -la"
5562
- workspace: "john/dev-env", command: "git status", timeout_ms: 30000
63+
- workspace: "my-workspace", command: "npm run dev", background: true
5664
- workspace: "my-workspace.main", command: "docker ps"`,
5765
Schema: aisdk.Schema{
5866
Properties: map[string]any{
@@ -70,6 +78,10 @@ Examples:
7078
"default": 60000,
7179
"minimum": 1,
7280
},
81+
"background": map[string]any{
82+
"type": "boolean",
83+
"description": "Whether to run the command in the background. The command will not be affected by the timeout.",
84+
},
7385
},
7486
Required: []string{"workspace", "command"},
7587
},
@@ -82,7 +94,7 @@ Examples:
8294
return WorkspaceBashResult{}, xerrors.New("command cannot be empty")
8395
}
8496

85-
ctx, cancel := context.WithTimeoutCause(ctx, 5*time.Minute, errors.New("MCP handler timeout after 5 min"))
97+
ctx, cancel := context.WithTimeoutCause(ctx, 5*time.Minute, xerrors.New("MCP handler timeout after 5 min"))
8698
defer cancel()
8799

88100
// Normalize workspace input to handle various formats
@@ -137,23 +149,41 @@ Examples:
137149

138150
// Set default timeout if not specified (60 seconds)
139151
timeoutMs := args.TimeoutMs
152+
defaultTimeoutMs := 60000
140153
if timeoutMs <= 0 {
141-
timeoutMs = 60000
154+
timeoutMs = defaultTimeoutMs
155+
}
156+
command := args.Command
157+
if args.Background {
158+
// Background commands are not affected by the timeout
159+
timeoutMs = defaultTimeoutMs
160+
encodedCommand := base64.StdEncoding.EncodeToString([]byte(args.Command))
161+
encodedScript := base64.StdEncoding.EncodeToString([]byte(backgroundScript))
162+
commandID, err := cryptorand.StringCharset(cryptorand.Human, 8)
163+
if err != nil {
164+
return WorkspaceBashResult{}, xerrors.Errorf("failed to generate command ID: %w", err)
165+
}
166+
command = fmt.Sprintf(
167+
"ARG_COMMAND=\"$(echo -n %s | base64 -d)\" ARG_COMMAND_ID=%s bash -c \"$(echo -n %s | base64 -d)\"",
168+
encodedCommand,
169+
commandID,
170+
encodedScript,
171+
)
142172
}
143173

144174
// Create context with timeout
145175
ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutMs)*time.Millisecond)
146176
defer cancel()
147177

148178
// Execute command with timeout handling
149-
output, err := executeCommandWithTimeout(ctx, session, args.Command)
179+
output, err := executeCommandWithTimeout(ctx, session, command)
150180
outputStr := strings.TrimSpace(string(output))
151181

152182
// Handle command execution results
153183
if err != nil {
154184
// Check if the command timed out
155185
if errors.Is(context.Cause(ctx), context.DeadlineExceeded) {
156-
outputStr += "\nCommand cancelled due to timeout"
186+
outputStr += "\nCommand canceled due to timeout"
157187
return WorkspaceBashResult{
158188
Output: outputStr,
159189
ExitCode: 124,
@@ -386,7 +416,7 @@ func executeCommandWithTimeout(ctx context.Context, session *gossh.Session, comm
386416
// Command completed normally
387417
return safeWriter.Bytes(), err
388418
case <-ctx.Done():
389-
// Context was cancelled (timeout or other cancellation)
419+
// Context was canceled (timeout or other cancellation)
390420
// Close the session to stop the command
391421
_ = session.Close()
392422

codersdk/toolsdk/bash_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,6 @@ func TestWorkspaceBashTimeoutIntegration(t *testing.T) {
335335
require.Equal(t, "normal command", result.Output)
336336

337337
// Should NOT contain timeout message
338-
require.NotContains(t, result.Output, "Command cancelled due to timeout")
338+
require.NotContains(t, result.Output, "Command canceled due to timeout")
339339
})
340340
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
3+
# This script is used to run a command in the background.
4+
5+
set -o errexit
6+
set -o pipefail
7+
8+
set -o nounset
9+
10+
COMMAND="$ARG_COMMAND"
11+
COMMAND_ID="$ARG_COMMAND_ID"
12+
13+
set +o nounset
14+
15+
LOG_DIR="/tmp/mcp-bg"
16+
LOG_PATH="$LOG_DIR/$COMMAND_ID.log"
17+
mkdir -p "$LOG_DIR"
18+
19+
nohup bash -c "$COMMAND" >"$LOG_PATH" 2>&1 &
20+
COMMAND_PID="$!"
21+
22+
echo "Command started with PID: $COMMAND_PID"
23+
echo "Log path: $LOG_PATH"

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy