aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2026-02-15 13:03:38 +0700
committerShulhan <ms@kilabit.info>2026-02-15 13:37:31 +0700
commit19d58e9a4c900aec1d63de45a655657c760b1235 (patch)
tree959af0093d9ed2aca4976c69deb25a2d48fca44b
parent827a2741e4fcb8e6feb5bf76acb20799c2913451 (diff)
downloadawwan-19d58e9a4c900aec1d63de45a655657c760b1235.tar.xz
all: fix data race in Play and in integration tests
When the Play command executed from the web user interface, there is a possibility that concurrent requests set the sshConfig field in Awwan struct at the same time. In the integration tests for TestAwwan_Play_withLocal and TestExecLocal_sudo, the data race happens when writing to the same buffer for stdout and stderr, so we split them into separate buffers. There is also data race in SSE connection, when handler for ExecuteTail write to the same buffer with worker keep alive. This has been fixed on pakakeh.go module.
-rw-r--r--Makefile8
-rw-r--r--awwan.go24
-rw-r--r--awwan_play_test.go33
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--sudo_test.go19
6 files changed, 49 insertions, 41 deletions
diff --git a/Makefile b/Makefile
index b35ae16..25b5b4d 100644
--- a/Makefile
+++ b/Makefile
@@ -70,10 +70,12 @@ build-awwan-test:
## time without rebuilding again.
.PHONY: test-integration
+test-integration: CGO_ENABLED=1
test-integration:
- go test -tags=integration -c .
+ go test -race -tags=integration -c .
machinectl shell awwan@awwan-test \
- /bin/sh -c "cd src; ./awwan.test"
+ /bin/sh -c "cd src; \
+ ./awwan.test -test.failfast -test.timeout=2m -test.v"
.PHONY: test-all
test-all: CGO_ENABLED=1
@@ -85,7 +87,7 @@ test-all:
go test -failfast -timeout=2m -race \
-coverprofile=$(COVER_OUT) \
-tags=integration \
- ./...
+ ./..."
go tool cover -html=$(COVER_OUT) -o $(COVER_HTML)
#}}}
diff --git a/awwan.go b/awwan.go
index 32da73f..6a5b5b3 100644
--- a/awwan.go
+++ b/awwan.go
@@ -68,9 +68,6 @@ const defTmpDirPlay = `~/.cache/awwan`
type Awwan struct {
cryptoc *cryptoContext
- // All the Host values from SSH config files.
- sshConfig *sshconfig.Config
-
httpd *httpServer
BaseDir string
@@ -407,6 +404,7 @@ func (aww *Awwan) Play(ctx context.Context, req *ExecRequest) (err error) {
sessionDir = filepath.Dir(req.scriptPath)
ses *Session
+ sshConfig *sshconfig.Config
sshSection *sshconfig.Section
pos linePosition
)
@@ -424,12 +422,12 @@ func (aww *Awwan) Play(ctx context.Context, req *ExecRequest) (err error) {
// Always load the SSH config, in case we run on "serve" and
// .ssh/config changed by user.
- err = aww.loadSSHConfig()
+ sshConfig, err = aww.loadSSHConfig()
if err != nil {
goto out
}
- sshSection = aww.sshConfig.Get(ses.hostname)
+ sshSection = sshConfig.Get(ses.hostname)
if sshSection == nil {
err = fmt.Errorf(`can not find Host %q in SSH config`, ses.hostname)
goto out
@@ -507,7 +505,7 @@ func (aww *Awwan) Stop() (err error) {
// loadSSHConfig load all SSH config from user's home and the awwan base
// directory.
-func (aww *Awwan) loadSSHConfig() (err error) {
+func (aww *Awwan) loadSSHConfig() (sshConfig *sshconfig.Config, err error) {
var (
logp = `loadSSHConfig`
@@ -518,27 +516,27 @@ func (aww *Awwan) loadSSHConfig() (err error) {
homeDir, err = os.UserHomeDir()
if err != nil {
- return fmt.Errorf("%s: %w", logp, err)
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
}
configFile = filepath.Join(homeDir, defSSHDir, defSSHConfig)
- aww.sshConfig, err = sshconfig.Load(configFile)
+ sshConfig, err = sshconfig.Load(configFile)
if err != nil {
- return fmt.Errorf("%s: %w", logp, err)
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
}
configFile = filepath.Join(aww.BaseDir, defSSHDir, defSSHConfig)
baseDirConfig, err = sshconfig.Load(configFile)
if err != nil {
- return fmt.Errorf("%s: %w", logp, err)
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
}
if baseDirConfig == nil {
- return nil
+ return nil, nil
}
- aww.sshConfig.Merge(baseDirConfig)
+ sshConfig.Merge(baseDirConfig)
- return nil
+ return sshConfig, nil
}
// lookupBaseDir find the directory that contains ".ssh" directory from
diff --git a/awwan_play_test.go b/awwan_play_test.go
index 00d407d..4ae0cfc 100644
--- a/awwan_play_test.go
+++ b/awwan_play_test.go
@@ -13,6 +13,7 @@ import (
"path/filepath"
"testing"
+ "git.sr.ht/~shulhan/pakakeh.go/lib/mlog"
"git.sr.ht/~shulhan/pakakeh.go/lib/test"
)
@@ -33,7 +34,8 @@ func TestAwwan_Play_withLocal(t *testing.T) {
type testCase struct {
scriptFile string
lineRange string
- expOutput string
+ expStdout string
+ expStderr string
expError string
}
@@ -60,37 +62,38 @@ func TestAwwan_Play_withLocal(t *testing.T) {
var cases = []testCase{{
scriptFile: filepath.Join(scriptDir, `play.aww`),
lineRange: `1-`,
- expOutput: string(tdata.Output[`play_with_local:output`]),
+ expStdout: string(tdata.Output[`play_with_local:stdout`]),
+ expStderr: string(tdata.Output[`play_with_local:stderr`]),
}, {
scriptFile: filepath.Join(scriptDir, `tmp`),
expError: `NewExecRequest: "testdata/play/awwanssh.test/tmp" is a directory`,
}}
- var (
- ctx = context.Background()
-
- c testCase
- req *ExecRequest
- logw bytes.Buffer
- )
+ var testerr bytes.Buffer
+ var namederr = mlog.NewNamedWriter(`testerr`, &testerr)
+ var testout bytes.Buffer
+ var namedout = mlog.NewNamedWriter(`testout`, &testout)
- for _, c = range cases {
- req, err = NewExecRequest(CommandModePlay, c.scriptFile, c.lineRange)
+ for _, c := range cases {
+ req, err := NewExecRequest(CommandModePlay, c.scriptFile, c.lineRange)
if err != nil {
test.Assert(t, `NewExecRequest: error`, c.expError, err.Error())
continue
}
- logw.Reset()
- req.registerLogWriter(`output`, &logw)
+ testerr.Reset()
+ testout.Reset()
+ req.mlog.RegisterErrorWriter(namederr)
+ req.mlog.RegisterOutputWriter(namedout)
- err = aww.Play(ctx, req)
+ err = aww.Play(context.Background(), req)
if err != nil {
test.Assert(t, `Play: error`, c.expError, err.Error())
continue
}
- test.Assert(t, `Local`, c.expOutput, logw.String())
+ test.Assert(t, `stdout`, c.expStdout, testout.String())
+ test.Assert(t, `stderr`, c.expStderr, testerr.String())
}
}
diff --git a/go.mod b/go.mod
index ce1095a..7786153 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ go 1.25.0
require (
git.sr.ht/~shulhan/ciigo v0.16.0
- git.sr.ht/~shulhan/pakakeh.go v0.61.1-0.20260211152820-e5a9e1e5314a
+ git.sr.ht/~shulhan/pakakeh.go v0.61.1-0.20260215055354-3f780768650c
github.com/evanw/esbuild v0.27.3
)
diff --git a/go.sum b/go.sum
index 3825d82..a59189c 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,8 @@ git.sr.ht/~shulhan/asciidoctor-go v0.7.3 h1:QjMMG3AgtnWkAIV2OqPfAksCdgonmY6cQXwy
git.sr.ht/~shulhan/asciidoctor-go v0.7.3/go.mod h1:fdqQrwicDfRycH6ovYIQ5NzwbFIryNSsrFn5Gw0IsOk=
git.sr.ht/~shulhan/ciigo v0.16.0 h1:TOwCaD9mm3hRxbVDsmJ46xRyUxLoH257ACI4M+RLcQo=
git.sr.ht/~shulhan/ciigo v0.16.0/go.mod h1:rgj8D5KwmfFw4kGWXnGTdUQatSWy/RUCriNGWz4mQRw=
-git.sr.ht/~shulhan/pakakeh.go v0.61.1-0.20260211152820-e5a9e1e5314a h1:VpuI0dK82MqHO5yaDZyfTn3gOU6noFW1wcJBjSsI5Ms=
-git.sr.ht/~shulhan/pakakeh.go v0.61.1-0.20260211152820-e5a9e1e5314a/go.mod h1:cU1ZnE54I0SaI3e1aHAmnMYIXq6N2c7yGbbMpFYOECI=
+git.sr.ht/~shulhan/pakakeh.go v0.61.1-0.20260215055354-3f780768650c h1:cIroMyQB7Gm68HFpYR2ZhYwvH/AQLNmzjHPVcXbV5lM=
+git.sr.ht/~shulhan/pakakeh.go v0.61.1-0.20260215055354-3f780768650c/go.mod h1:cU1ZnE54I0SaI3e1aHAmnMYIXq6N2c7yGbbMpFYOECI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/evanw/esbuild v0.27.3 h1:dH/to9tBKybig6hl25hg4SKIWP7U8COdJKbGEwnUkmU=
github.com/evanw/esbuild v0.27.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
diff --git a/sudo_test.go b/sudo_test.go
index dd5f0dd..3d6fb09 100644
--- a/sudo_test.go
+++ b/sudo_test.go
@@ -10,6 +10,7 @@ import (
"context"
"testing"
+ "git.sr.ht/~shulhan/pakakeh.go/lib/mlog"
"git.sr.ht/~shulhan/pakakeh.go/lib/test"
)
@@ -23,16 +24,20 @@ func TestExecLocal_sudo(t *testing.T) {
}
var (
- mockin = &mockStdin{}
- mockout = &bytes.Buffer{}
- req = &ExecRequest{
+ mockin = &mockStdin{}
+ req = &ExecRequest{
stdin: mockin,
}
err error
)
req.init(`.`)
- req.registerLogWriter(`output`, mockout)
+ var mockStderr bytes.Buffer
+ var namederr = mlog.NewNamedWriter(`testerr`, &mockStderr)
+ var mockStdout bytes.Buffer
+ var namedout = mlog.NewNamedWriter(`testout`, &mockStdout)
+ req.mlog.RegisterErrorWriter(namederr)
+ req.mlog.RegisterOutputWriter(namedout)
var cases = []testCase{{
desc: `SingleSudo`,
@@ -96,20 +101,20 @@ func TestExecLocal_sudo(t *testing.T) {
for _, c = range cases {
t.Log(c.desc)
- mockout.Reset()
+ mockStderr.Reset()
+ mockStdout.Reset()
mockin.buf.Reset()
mockin.buf.WriteString(c.sudoPass)
for x, stmt = range c.listStmt {
err = ExecLocal(ctx, req, &stmt)
if err != nil {
- t.Log(mockout.String())
var expError = c.expError[x]
test.Assert(t, `error`, expError, err.Error())
}
req.mlog.Flush()
}
- test.Assert(t, c.desc+` output`, c.expOutput, mockout.String())
+ test.Assert(t, c.desc+` stdout`, c.expOutput, mockStderr.String())
}
}