Skip to content
Merged
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ tool (

require (
connectrpc.com/connect v1.20.0
github.com/Microsoft/go-winio v0.6.2
github.com/alecthomas/chroma/v2 v2.14.0
github.com/carapace-sh/carapace v1.11.6
github.com/chainreactors/crtm v0.0.3-0.20260618163257-073207497076
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ github.com/M09ic/go-ntlmssp v0.0.0-20230312133735-dcccd454dfe0 h1:9Y+BdzDIHfpKy0
github.com/M09ic/go-ntlmssp v0.0.0-20230312133735-dcccd454dfe0/go.mod h1:yMNEF6ulbFipt3CakMhcmcNVACshPRG4Ap4l00V+mMs=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub47e7kd2PLZeACxc1LkiiNoDOFRClE=
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2pivrPICvLOuROKmlqURtFIEsoJZaMidQfCG1+D4=
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8=
Expand Down
93 changes: 90 additions & 3 deletions pkg/commands/bash.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/exec"
"sort"
"strings"
"sync"
"time"

"github.com/chainreactors/aiscan/agent/inbox"
Expand Down Expand Up @@ -44,6 +45,10 @@ type BashTool struct {
tasks *tmux.Manager
commandNames func() []string
resolveCommand func(string) (Command, bool)
shellRegistry *CommandRegistry
adapterMu sync.Mutex
shellAdapter *shellCommandAdapter
closeOnce sync.Once
}

func NewBashTool(workDir string, timeout int) *BashTool {
Expand All @@ -60,7 +65,47 @@ func (t *BashTool) SetCommandResolver(fn func(string) (Command, bool)) {
t.resolveCommand = fn
}
func (t *BashTool) Name() string { return "bash" }
func (t *BashTool) Close() { t.tasks.Shutdown() }
func (t *BashTool) Close() {
t.closeOnce.Do(func() {
t.adapterMu.Lock()
adapter := t.shellAdapter
if adapter != nil {
adapter.shutdown()
}
t.adapterMu.Unlock()
t.tasks.Shutdown()
if adapter != nil {
adapter.cleanup()
}
})
}

func (t *BashTool) attachShellCommands(registry *CommandRegistry) {
t.shellRegistry = registry
}

func (t *BashTool) ensureShellCommands() (*shellCommandAdapter, error) {
if t.shellRegistry == nil {
return nil, nil
}
t.adapterMu.Lock()
defer t.adapterMu.Unlock()
if t.shellAdapter == nil {
adapter, err := newShellCommandAdapter(t.shellRegistry)
if err != nil {
return nil, err
}
t.shellAdapter = adapter
}
if t.commandNames != nil {
if err := t.shellAdapter.syncAliases(t.commandNames()); err != nil {
t.shellAdapter.close()
t.shellAdapter = nil
return nil, err
}
}
return t.shellAdapter, nil
}

func (t *BashTool) WithScannerProxy(proxy string) *BashTool {
t.scannerProxy = proxy
Expand Down Expand Up @@ -208,9 +253,39 @@ func (t *BashTool) Start(ctx context.Context, command string, options BashExecOp
if workDir == "" {
workDir = t.workDir
}
env := t.runEnv(options.Env)
left, right, hasPipe := splitPipeline(command)
leftToken := firstCommandToken(left)
if !hasPipe {
if cmd, ok := t.resolve(leftToken); ok {
if tokens, err := SplitCommandLine(left); err == nil {
if args, syntaxErr := stripShellSyntax(tokens[1:]); syntaxErr == nil {
args = normalizeNoColor(cmd.Name, args)
return t.startBuiltin(ctx, cmd, args, timeout, workDir, t.runEnv(options.Env, nil, ""), options)
}
}
}
}
adapter, err := t.ensureShellCommands()
if err != nil {
return nil, err
}
if adapter != nil {
contextID := adapter.retainContext(ctx)
env := t.runEnv(options.Env, adapter, contextID)
execution := newExecution(t.tasks, command, nil, workDir, env)
info, err := t.tasks.Create(workDir, command, options.Name, timeout, env, "")
if err != nil {
adapter.releaseContext(contextID)
return nil, err
}
execution.bind(info)
go func() {
<-t.tasks.Done(execution.ID)
adapter.releaseContext(contextID)
}()
return execution, nil
}
env := t.runEnv(options.Env, nil, "")
if cmd, ok := t.resolve(leftToken); ok {
tokens, err := SplitCommandLine(left)
if err != nil {
Expand Down Expand Up @@ -447,7 +522,7 @@ func (t *BashTool) collectResult(execution *Execution) *coretool.Result {
return result
}

func (t *BashTool) runEnv(overrides map[string]string) []string {
func (t *BashTool) runEnv(overrides map[string]string, adapter *shellCommandAdapter, shellContextID string) []string {
values := make(map[string]string)
for _, item := range t.proxyEnv() {
if key, value, ok := strings.Cut(item, "="); ok {
Expand All @@ -457,6 +532,18 @@ func (t *BashTool) runEnv(overrides map[string]string) []string {
for key, value := range overrides {
values[key] = value
}
if adapter != nil && shellContextID != "" {
for _, item := range adapter.environment(shellContextID) {
if key, value, ok := strings.Cut(item, "="); ok {
values[key] = value
}
}
path := values["PATH"]
if path == "" {
path = os.Getenv("PATH")
}
values["PATH"] = adapter.runtimeDir + string(os.PathListSeparator) + path
}
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
Expand Down
4 changes: 3 additions & 1 deletion pkg/commands/bash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,9 @@ func TestBashExecOptionsAreIsolatedAcrossConcurrentCalls(t *testing.T) {
wg.Add(1)
go func(i int) {
defer wg.Done()
results[i], errs[i] = bash.RunForeground(context.Background(), `printf '%s\n' "$AISCAN_RUN_VALUE"; pwd`, BashExecOptions{
// Keep the short-lived shell alive until the PTY reader is scheduled;
// this test exercises concurrent option isolation, not PTY drain timing.
results[i], errs[i] = bash.RunForeground(context.Background(), `printf '%s\n' "$AISCAN_RUN_VALUE"; pwd; sleep 0.05`, BashExecOptions{
WorkDir: dirs[i],
Env: map[string]string{"AISCAN_RUN_VALUE": fmt.Sprintf("value-%d", i)},
OnOutput: func(data []byte) {
Expand Down
1 change: 1 addition & 0 deletions pkg/commands/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func init() {
bash.SetCommandNames(reg.Names)
bash.SetCommandResolver(reg.Get)
reg.RegisterTool(bash)
bash.attachShellCommands(reg)

tmuxCmd := NewTmuxCommand(bash)
reg.Register(tmuxCmd, "core")
Expand Down
17 changes: 17 additions & 0 deletions pkg/commands/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,20 @@ func TestNativeListToolIsRunnerOnly(t *testing.T) {
t.Fatal("runner mode must expose the native ls tool")
}
}

func TestCoreFactoryAttachesRegisteredCommandsToShell(t *testing.T) {
registry := NewRegistry()
BuildPlan(capability.Select(capability.Options{Groups: []string{"core"}}), &Deps{WorkDir: t.TempDir()}, registry)
defer closeRegistryTools(registry)
tool, ok := registry.GetTool("bash")
if !ok {
t.Fatal("core factory did not register bash")
}
bash := tool.(*BashTool)
if bash.shellRegistry != registry {
t.Fatal("registered commands were not attached to bash")
}
if bash.shellAdapter != nil {
t.Fatal("shell adapter must remain lazy until a real shell is needed")
}
}
Loading