Reapply "fix: harden WSL2 work root and sync"

This reverts commit bbb8183eca.
This commit is contained in:
Vincent Koc 2026-05-04 23:33:39 -07:00
parent 0ca412a8d5
commit 15c10ee2e2
No known key found for this signature in database
7 changed files with 184 additions and 9 deletions

View File

@ -16,6 +16,9 @@
- Fixed Windows WebVNC credential handling so generated portal links preserve special characters and managed TightVNC sessions copy service passwords into the logged-in user's registry profile.
- Fixed managed Linux browser setup so Chrome/Chromium launches skip first-run and default-browser prompts.
- Fixed WebVNC portal passwords with escaped special characters and kept the bridge alive across viewer resets and transient coordinator EOFs.
- Fixed managed AWS Windows WSL2 bootstrap by using the current Ubuntu WSL rootfs URL, downloading large rootfs files through `curl.exe`, and retrying empty or partial rootfs downloads instead of reusing a poisoned tarball. Thanks @vincentkoc.
- Fixed AWS Windows WSL2 mode overrides so they refresh the default instance type to a nested-virtualization-capable family. Thanks @steipete.
- Fixed AWS Windows WSL2 runs so mode overrides also refresh the default work root to `/work/crabbox` and sync via a WSL archive stream instead of rsync's remote protocol through Windows OpenSSH.
## 0.5.0 - 2026-05-04
@ -49,8 +52,6 @@
- Fixed failed Blacksmith Testbox warmups so printed, newly listed, or delayed `tbx_...` boxes are stopped instead of being left queued after an upstream workflow error.
- Fixed `crabbox run --junit` so all-passing JUnit files record results instead of leaving the coordinator run stuck when the failure list is empty.
- Fixed native Windows `--shell` runs so multi-statement PowerShell scripts keep their quotes instead of being re-parsed by a nested PowerShell process.
- Fixed managed AWS Windows WSL2 bootstrap by using the current Ubuntu WSL rootfs URL, downloading large rootfs files through `curl.exe`, and retrying empty or partial rootfs downloads instead of reusing a poisoned tarball. Thanks @vincentkoc.
- Fixed AWS Windows WSL2 mode overrides so they refresh the default instance type to a nested-virtualization-capable family. Thanks @steipete.
- Removed the static macOS managed-login path so static host VNC cannot be mistaken for a Crabbox-created external instance.
- Excluded macOS AppleDouble `._*` sidecar files from default sync manifests so native Windows archives do not transfer invalid TypeScript/package sidecars.
- Quoted `crabbox vnc` tunnel key paths so macOS `Application Support` lease keys can be pasted directly into a shell.

View File

@ -182,7 +182,7 @@ func baseConfig() Config {
SSHPort: "2222",
SSHFallbackPorts: []string{"22"},
ProviderKey: "crabbox-steipete",
WorkRoot: "/work/crabbox",
WorkRoot: defaultPOSIXWorkRoot,
TTL: 90 * time.Minute,
IdleTimeout: 30 * time.Minute,
Sync: SyncConfig{

View File

@ -374,6 +374,17 @@ func (a App) runCommand(ctx context.Context, args []string) (err error) {
recorder.Event("sync.finished", "synced", fmt.Sprintf("duration=%s mode=archive", timings.sync.Round(time.Millisecond)))
goto afterSync
}
if isWindowsWSL2Target(target) {
stepStart = time.Now()
if err := syncWindowsWSL2(ctx, target, repo, cfg, workdir, manifest, a.Stdout, a.Stderr, rsyncOptions{Debug: *debugSync, Delete: cfg.Sync.Delete, Checksum: cfg.Sync.Checksum, Timeout: cfg.Sync.Timeout, HeartbeatInterval: 15 * time.Second}); err != nil {
return recordFailure(err)
}
timings.syncSteps.rsync = time.Since(stepStart)
timings.sync = time.Since(syncStart)
fmt.Fprintf(a.Stderr, "sync complete in %s\n", timings.sync.Round(time.Millisecond))
recorder.Event("sync.finished", "synced", fmt.Sprintf("duration=%s mode=archive-wsl2", timings.sync.Round(time.Millisecond)))
goto afterSync
}
fingerprint := ""
if cfg.Sync.Fingerprint {
stepStart = time.Now()

View File

@ -168,6 +168,7 @@ func TestApplyServerTypeFlagOverridesUsesTargetAwareAWSDefaults(t *testing.T) {
WindowsMode: windowsModeNormal,
Class: "beast",
ServerType: "c7a.48xlarge",
WorkRoot: defaultWindowsWorkRoot,
}
fs := newFlagSet("test", io.Discard)
provider := fs.String("provider", cfg.Provider, "")
@ -186,6 +187,9 @@ func TestApplyServerTypeFlagOverridesUsesTargetAwareAWSDefaults(t *testing.T) {
if cfg.ServerType != tt.want {
t.Fatalf("serverType=%q want %q", cfg.ServerType, tt.want)
}
if cfg.WindowsMode == windowsModeWSL2 && cfg.WorkRoot != defaultPOSIXWorkRoot {
t.Fatalf("workRoot=%q want %q", cfg.WorkRoot, defaultPOSIXWorkRoot)
}
if cfg.ServerTypeExplicit {
t.Fatal("ServerTypeExplicit=true, want false")
}
@ -193,6 +197,62 @@ func TestApplyServerTypeFlagOverridesUsesTargetAwareAWSDefaults(t *testing.T) {
}
}
func TestApplyTargetFlagOverridesRefreshesDefaultWorkRoot(t *testing.T) {
tests := []struct {
name string
cfg Config
args []string
want string
}{
{
name: "native windows to wsl2",
cfg: Config{
TargetOS: targetWindows,
WindowsMode: windowsModeNormal,
WorkRoot: defaultWindowsWorkRoot,
},
args: []string{"--windows-mode", "wsl2"},
want: defaultPOSIXWorkRoot,
},
{
name: "wsl2 to native windows",
cfg: Config{
TargetOS: targetWindows,
WindowsMode: windowsModeWSL2,
WorkRoot: defaultPOSIXWorkRoot,
},
args: []string{"--windows-mode", "normal"},
want: defaultWindowsWorkRoot,
},
{
name: "custom root is preserved",
cfg: Config{
TargetOS: targetWindows,
WindowsMode: windowsModeNormal,
WorkRoot: `/custom/root`,
},
args: []string{"--windows-mode", "wsl2"},
want: `/custom/root`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs := newFlagSet("test", io.Discard)
targetFlags := registerTargetFlags(fs, tt.cfg)
if err := parseFlags(fs, tt.args); err != nil {
t.Fatal(err)
}
cfg := tt.cfg
if err := applyTargetFlagOverrides(&cfg, fs, targetFlags); err != nil {
t.Fatal(err)
}
if cfg.WorkRoot != tt.want {
t.Fatalf("workRoot=%q want %q", cfg.WorkRoot, tt.want)
}
})
}
}
func TestApplyServerTypeFlagOverridesPreservesExplicitType(t *testing.T) {
cfg := Config{
Provider: "aws",

View File

@ -382,6 +382,26 @@ func TestRemoteApplySyncManifestOnlyCommitsManifest(t *testing.T) {
}
}
func TestPOSIXPrepareWorkdirDeletesButPreservesGit(t *testing.T) {
workdir := t.TempDir()
mustWriteTestFile(t, filepath.Join(workdir, ".git", "config"), "git")
mustWriteTestFile(t, filepath.Join(workdir, "stale.txt"), "stale")
mustWriteTestFile(t, filepath.Join(workdir, "old", "nested.txt"), "old")
cmd := exec.Command("bash", "-lc", posixPrepareWorkdir(workdir, true))
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("posix prepare failed: %v\n%s", err, out)
}
if _, err := os.Stat(filepath.Join(workdir, ".git", "config")); err != nil {
t.Fatalf(".git should survive prepare: %v", err)
}
for _, rel := range []string{"stale.txt", "old"} {
if _, err := os.Stat(filepath.Join(workdir, rel)); !os.IsNotExist(err) {
t.Fatalf("%s should be removed, stat err=%v", rel, err)
}
}
}
func TestRemoteGitHydrateStatusUsesMarkerAndRemoteBase(t *testing.T) {
got := remoteGitHydrateStatus("/work/repo", "main", "abc123")
for _, want := range []string{

View File

@ -45,10 +45,78 @@ func syncWindowsNative(ctx context.Context, target SSHTarget, repo Repo, cfg Con
return nil
}
func syncWindowsWSL2(ctx context.Context, target SSHTarget, repo Repo, cfg Config, workdir string, manifest SyncManifest, stdout, stderr anyWriter, opts rsyncOptions) error {
if err := runSSHQuiet(ctx, target, posixPrepareWorkdir(workdir, cfg.Sync.Delete)); err != nil {
return exit(7, "prepare remote workdir: %v", err)
}
if cfg.Sync.GitSeed {
if err := runSSHQuiet(ctx, target, remoteGitSeed(workdir, repo.RemoteURL, repo.Head)); err != nil {
fmt.Fprintf(stderr, "warning: remote git seed failed: %v\n", err)
}
}
if err := syncArchive(ctx, target, repo, workdir, manifest, stdout, stderr, opts); err != nil {
return err
}
if out, err := runSSHCombinedOutput(ctx, target, remoteSyncSanity(workdir, false)); err != nil {
if out != "" {
return exit(6, "remote sync sanity failed: %s: %v", out, err)
}
return exit(6, "remote sync sanity failed: %v", err)
}
return nil
}
func syncArchive(ctx context.Context, target SSHTarget, repo Repo, workdir string, manifest SyncManifest, stdout, stderr anyWriter, opts rsyncOptions) error {
var input bytes.Buffer
input.Write(manifest.NUL())
cmd := exec.CommandContext(ctx, "tar", "-czf", "-", "-C", repo.Root, "--null", "-T", "-")
cmd.Stdin = &input
var archive bytes.Buffer
cmd.Stdout = &archive
cmd.Stderr = stderr
start := time.Now()
if err := cmd.Run(); err != nil {
return exit(6, "create sync archive: %v", err)
}
if opts.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, opts.Timeout)
defer cancel()
}
stopHeartbeat := startSyncHeartbeat(stderr, start, opts.HeartbeatInterval)
err := runSSHInput(ctx, target, posixExtractArchive(workdir), &archive, stdout, stderr)
stopHeartbeat()
if ctx.Err() == context.DeadlineExceeded {
return exit(6, "archive sync timed out after %s", opts.Timeout)
}
if err != nil {
return exit(6, "archive sync failed: %v", err)
}
return nil
}
type anyWriter interface {
Write([]byte) (int, error)
}
func posixPrepareWorkdir(workdir string, delete bool) string {
deleteScript := ""
if delete {
deleteScript = `
find . -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf -- {} +
`
}
return "bash -lc " + shellQuote(`set -e
mkdir -p `+shellQuote(workdir)+`
cd `+shellQuote(workdir)+deleteScript)
}
func posixExtractArchive(workdir string) string {
return "bash -lc " + shellQuote(`set -e
mkdir -p `+shellQuote(workdir)+`
tar -xzf - -C `+shellQuote(workdir))
}
func windowsPrepareWorkdir(workdir string, delete bool) string {
deleteScript := ""
if delete {

View File

@ -12,17 +12,16 @@ const (
windowsModeNormal = "normal"
windowsModeWSL2 = "wsl2"
defaultPOSIXWorkRoot = "/work/crabbox"
defaultWindowsWorkRoot = `C:\crabbox`
)
func normalizeTargetConfig(cfg *Config) {
cfg.TargetOS = normalizeTargetOS(cfg.TargetOS)
cfg.WindowsMode = normalizeWindowsMode(cfg.WindowsMode)
if cfg.TargetOS == targetWindows && cfg.WorkRoot == "/work/crabbox" {
if cfg.WindowsMode == windowsModeWSL2 {
cfg.WorkRoot = "/work/crabbox"
} else {
cfg.WorkRoot = `C:\crabbox`
}
if isDefaultWorkRoot(cfg.WorkRoot) {
cfg.WorkRoot = defaultWorkRootForTarget(cfg.TargetOS, cfg.WindowsMode)
}
if cfg.Provider == "aws" && cfg.TargetOS == targetMacOS && cfg.SSHUser == baseConfig().SSHUser {
cfg.SSHUser = "ec2-user"
@ -41,6 +40,22 @@ func normalizeTargetConfig(cfg *Config) {
}
}
func isDefaultWorkRoot(value string) bool {
switch value {
case "", defaultPOSIXWorkRoot, defaultWindowsWorkRoot:
return true
default:
return false
}
}
func defaultWorkRootForTarget(targetOS, windowsMode string) string {
if targetOS == targetWindows && windowsMode == windowsModeNormal {
return defaultWindowsWorkRoot
}
return defaultPOSIXWorkRoot
}
func normalizeTargetOS(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "linux", "ubuntu":