Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7793767625 |
@@ -2,14 +2,10 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
|
"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
|
||||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/restic"
|
"gitea.dcglab.co.uk/steve/restic-manager/internal/restic"
|
||||||
@@ -47,25 +43,23 @@ func (s *fakeSender) snapshot() []api.Envelope {
|
|||||||
// setupScript writes a shell script (without shebang) to a temp dir,
|
// setupScript writes a shell script (without shebang) to a temp dir,
|
||||||
// names it "restic", makes it executable, and returns the path.
|
// names it "restic", makes it executable, and returns the path.
|
||||||
//
|
//
|
||||||
// Writes to "<path>.tmp" then renames into place. The rename is the
|
// Writes to "<path>.tmp" then renames into place. The rename is what
|
||||||
// usual guard against ETXTBSY: under -race + many t.Parallel tests,
|
// makes this race-free: under -race + many t.Parallel tests, a
|
||||||
// a fork-from-another-goroutine can inherit the writable fd from
|
// fork-from-another-goroutine can inherit the writable fd from
|
||||||
// os.WriteFile before close completes, and exec'ing the file then
|
// os.WriteFile before close completes, and exec'ing the file then
|
||||||
// returns ETXTBSY ("text file busy"). The renamed dirent points at
|
// returns ETXTBSY ("text file busy"). Once the rename lands, the
|
||||||
// an inode that has no writable fd open anywhere — exec is safe on
|
// final path is a fresh dirent pointing at an inode that has no
|
||||||
// a vanilla filesystem.
|
// writable fd open anywhere — exec is safe.
|
||||||
//
|
//
|
||||||
// On overlayfs (every job that runs inside a `container:` block on
|
// Temp dirs live under /dev/shm when it's writable (tmpfs). On a
|
||||||
// our Gitea runner), the rename can briefly leak ETXTBSY anyway —
|
// containerised CI runner, t.TempDir() lands on overlayfs, which
|
||||||
// the upper layer's "writable inode" bookkeeping lags the userspace
|
// has its own ETXTBSY surface that the rename pattern above does
|
||||||
// close. To make the helper deterministic across environments, we
|
// not fully close — the kernel's "writable inode" bookkeeping
|
||||||
// probe-exec the file with a benign argument until exec succeeds,
|
// can lag the userspace close on overlay's upper layer. Falling
|
||||||
// then return. Each script body has a `case "$1" in ... esac` shape
|
// back to a real tmpfs sidesteps the problem entirely.
|
||||||
// where unknown args fall through to a clean exit, so the probe is
|
|
||||||
// a no-op from the test's point of view.
|
|
||||||
func setupScript(t *testing.T, body string) string {
|
func setupScript(t *testing.T, body string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dir := t.TempDir()
|
dir := tempDirForExec(t)
|
||||||
final := filepath.Join(dir, "restic")
|
final := filepath.Join(dir, "restic")
|
||||||
tmp := final + ".tmp"
|
tmp := final + ".tmp"
|
||||||
if err := os.WriteFile(tmp, []byte("#!/bin/sh\n"+body+"\n"), 0o755); err != nil {
|
if err := os.WriteFile(tmp, []byte("#!/bin/sh\n"+body+"\n"), 0o755); err != nil {
|
||||||
@@ -74,21 +68,22 @@ func setupScript(t *testing.T, body string) string {
|
|||||||
if err := os.Rename(tmp, final); err != nil {
|
if err := os.Rename(tmp, final); err != nil {
|
||||||
t.Fatalf("setupScript: rename: %v", err)
|
t.Fatalf("setupScript: rename: %v", err)
|
||||||
}
|
}
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
deadline := time.Now().Add(3 * time.Second)
|
// tempDirForExec returns a per-test temp dir that's safe to write
|
||||||
for {
|
// then exec from. Uses /dev/shm (tmpfs) when available, otherwise
|
||||||
err := exec.Command(final, "__rm_probe__").Run()
|
// falls back to t.TempDir(). See the setupScript comment for why.
|
||||||
|
func tempDirForExec(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
if st, err := os.Stat("/dev/shm"); err == nil && st.IsDir() {
|
||||||
|
dir, err := os.MkdirTemp("/dev/shm", "rmrunner-")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return final
|
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
||||||
|
return dir
|
||||||
}
|
}
|
||||||
if !errors.Is(err, syscall.ETXTBSY) {
|
|
||||||
t.Fatalf("setupScript: probe exec: %v", err)
|
|
||||||
}
|
|
||||||
if time.Now().After(deadline) {
|
|
||||||
t.Fatalf("setupScript: %s still ETXTBSY after 3s", final)
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
|
return t.TempDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
// firstEnvOfType returns the first envelope with the given type, or
|
// firstEnvOfType returns the first envelope with the given type, or
|
||||||
|
|||||||
Reference in New Issue
Block a user