mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 13:31:26 -10:00
Reject starts when all slots are live or reserved, prune only completed history, and release reservations on launch failure. Keep live processes tracked even after failed termination. Add concurrent admission and real-process regressions for #1085. Amp-Thread-ID: https://ampcode.com/threads/T-01a0dcd5-eb93-74c9-a1ac-5b140edaf973 Co-authored-by: Amp <[email protected]>
537 lines
18 KiB
Go
537 lines
18 KiB
Go
package execution
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func processManagerRequest(root string, command *exec.Cmd) Request {
|
|
return Request{
|
|
Origin: OriginInteractiveCommand, Mode: ModeCaptured,
|
|
Command: Command{Name: command.Path, Args: command.Args[1:]},
|
|
WorkingDirectory: root, WorkspaceRoots: []string{root},
|
|
Approval: ApprovalContext{PolicyVersion: PolicyVersion},
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerReservesCapacityBeforeTransport(t *testing.T) {
|
|
root := t.TempDir()
|
|
manager := NewProcessManager(ProcessManagerOptions{MaxProcesses: 1})
|
|
entered := make(chan struct{}, 20)
|
|
release := make(chan struct{})
|
|
launchErr := errors.New("transport failed")
|
|
manager.startTransport = func(*exec.Cmd, io.Writer, bool) (io.WriteCloser, bool, func(), error) {
|
|
entered <- struct{}{}
|
|
<-release
|
|
return nil, false, nil, launchErr
|
|
}
|
|
start := func() error {
|
|
command := exec.Command(os.Args[0])
|
|
cleaned := false
|
|
_, err := manager.Start(context.Background(), ProcessStart{
|
|
Prepared: PreparedCommand{Command: command, Cleanup: func() { cleaned = true }},
|
|
Request: processManagerRequest(root, command),
|
|
}, 0)
|
|
if !cleaned {
|
|
t.Error("failed admission or launch did not clean prepared resources")
|
|
}
|
|
return err
|
|
}
|
|
first := make(chan error, 1)
|
|
go func() { first <- start() }()
|
|
<-entered
|
|
results := make(chan error, 16)
|
|
for range 16 {
|
|
go func() { results <- start() }()
|
|
}
|
|
// Release blocked transports even when testing the unfixed implementation.
|
|
select {
|
|
case <-entered:
|
|
close(release)
|
|
<-first
|
|
for range 16 {
|
|
<-results
|
|
}
|
|
t.Fatal("additional transport launched while the only slot was reserved")
|
|
case err := <-results:
|
|
if err == nil || errors.Is(err, launchErr) {
|
|
t.Errorf("full manager returned %v, want capacity error", err)
|
|
}
|
|
}
|
|
for range 15 {
|
|
if err := <-results; err == nil || errors.Is(err, launchErr) {
|
|
t.Errorf("full manager returned %v, want capacity error", err)
|
|
}
|
|
}
|
|
close(release)
|
|
if err := <-first; !errors.Is(err, launchErr) {
|
|
t.Fatalf("first launch = %v", err)
|
|
}
|
|
if err := start(); !errors.Is(err, launchErr) {
|
|
t.Fatalf("failed launch did not release reservation: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerCapacityDoesNotEvictLiveProcesses(t *testing.T) {
|
|
for _, limit := range []int{1, 9} {
|
|
t.Run(fmt.Sprint(limit), func(t *testing.T) {
|
|
root := t.TempDir()
|
|
manager := NewProcessManager(ProcessManagerOptions{MaxProcesses: limit})
|
|
kills := 0
|
|
for id := range limit {
|
|
manager.processes[id] = &managedProcess{
|
|
id: id, command: &exec.Cmd{Process: &os.Process{Pid: 123}},
|
|
done: make(chan struct{}), output: newProcessOutputBuffer(),
|
|
kill: func(int) error { kills++; return errors.New("kill failed") },
|
|
}
|
|
}
|
|
manager.Stop(0) // A failed kill must not free capacity.
|
|
manager.Remove(0) // Nor may removing a live identity bypass the limit.
|
|
launches := 0
|
|
launchErr := errors.New("unexpected launch")
|
|
manager.startTransport = func(*exec.Cmd, io.Writer, bool) (io.WriteCloser, bool, func(), error) {
|
|
launches++
|
|
return nil, false, nil, launchErr
|
|
}
|
|
command := exec.Command(os.Args[0])
|
|
input := ProcessStart{Prepared: PreparedCommand{Command: command}, Request: processManagerRequest(root, command)}
|
|
if _, err := manager.Start(context.Background(), input, 0); err == nil || errors.Is(err, launchErr) {
|
|
t.Fatalf("full manager returned %v, want capacity error before launch", err)
|
|
}
|
|
if launches != 0 || kills != 1 || manager.Len() != limit {
|
|
t.Fatalf("launches=%d kills=%d retained=%d", launches, kills, manager.Len())
|
|
}
|
|
manager.processes[0].markDone(nil, 0, AdapterReport{}, nil, nil)
|
|
if _, err := manager.Start(context.Background(), input, 0); !errors.Is(err, launchErr) {
|
|
t.Fatalf("completed history did not free capacity: %v", err)
|
|
}
|
|
if launches != 1 || manager.Len() != limit-1 {
|
|
t.Fatalf("launches=%d retained=%d after completion", launches, manager.Len())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerCapacityHelper(t *testing.T) {
|
|
if os.Getenv("ZERO_PROCESS_CAPACITY_HELPER") != "1" {
|
|
return
|
|
}
|
|
time.Sleep(time.Minute)
|
|
os.Exit(0)
|
|
}
|
|
|
|
func TestProcessManagerCapacityWithRunningProcess(t *testing.T) {
|
|
root := t.TempDir()
|
|
manager := NewProcessManager(ProcessManagerOptions{MaxProcesses: 1})
|
|
t.Cleanup(func() { manager.StopAll() })
|
|
start := func() (ProcessResult, *exec.Cmd, error) {
|
|
command := exec.Command(os.Args[0], "-test.run=^TestProcessManagerCapacityHelper$")
|
|
command.Env = append(os.Environ(), "ZERO_PROCESS_CAPACITY_HELPER=1")
|
|
result, err := manager.Start(context.Background(), ProcessStart{
|
|
Prepared: PreparedCommand{Command: command}, Request: processManagerRequest(root, command),
|
|
}, 0)
|
|
return result, command, err
|
|
}
|
|
first, _, err := start()
|
|
if err != nil || first.Exited {
|
|
t.Fatalf("first start = %+v, %v", first, err)
|
|
}
|
|
if _, command, err := start(); err == nil || command.Process != nil {
|
|
t.Fatalf("second start = %v, process=%v; want rejection before OS launch", err, command.Process)
|
|
}
|
|
if got := len(manager.List()); got != 1 {
|
|
t.Fatalf("live processes = %d, want 1", got)
|
|
}
|
|
stopped, err := manager.Continue(context.Background(), ProcessContinue{
|
|
ProcessID: first.ProcessID, Interrupt: true, Wait: 10 * time.Second,
|
|
})
|
|
if err != nil || !stopped.Exited {
|
|
t.Fatalf("stop = %+v, %v", stopped, err)
|
|
}
|
|
if next, _, err := start(); err != nil || next.Exited {
|
|
t.Fatalf("start after completion = %+v, %v", next, err)
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerRetainsAndContinuesWithStableIdentity(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test command uses a POSIX shell")
|
|
}
|
|
root := t.TempDir()
|
|
manager := NewProcessManager(ProcessManagerOptions{CompletedRetention: time.Second})
|
|
command := exec.Command("/bin/sh", "-c", "printf first; sleep 0.05; printf second")
|
|
request := Request{
|
|
Origin: OriginInteractiveCommand, Mode: ModeCaptured,
|
|
Command: Command{Name: "/bin/sh", Args: []string{"-c", "printf first; sleep 0.05; printf second"}},
|
|
WorkingDirectory: root, WorkspaceRoots: []string{root},
|
|
Approval: ApprovalContext{PolicyVersion: PolicyVersion},
|
|
}
|
|
started, err := manager.Start(context.Background(), ProcessStart{
|
|
Prepared: PreparedCommand{Command: command}, Request: request, CommandText: "test",
|
|
}, time.Millisecond)
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if started.Exited || started.ProcessID < 1000 {
|
|
t.Fatalf("initial result = %#v, want retained process", started)
|
|
}
|
|
continued, err := manager.Continue(context.Background(), ProcessContinue{ProcessID: started.ProcessID, Wait: time.Second})
|
|
if err != nil {
|
|
t.Fatalf("Continue: %v", err)
|
|
}
|
|
if !continued.Exited || continued.ProcessID != started.ProcessID {
|
|
t.Fatalf("continued result = %#v, want same completed process", continued)
|
|
}
|
|
combined := started.Output + continued.Output
|
|
if strings.Count(combined, "first") != 1 || strings.Count(combined, "second") != 1 {
|
|
t.Fatalf("output was lost or duplicated: %q", combined)
|
|
}
|
|
if manager.Len() != 0 {
|
|
t.Fatalf("completed process still retained: %d", manager.Len())
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerInterruptsRetainedProcess(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test command uses a POSIX shell")
|
|
}
|
|
root := t.TempDir()
|
|
manager := NewProcessManager(ProcessManagerOptions{})
|
|
command := exec.Command("/bin/sh", "-c", "sleep 30")
|
|
request := Request{
|
|
Origin: OriginInteractiveCommand, Mode: ModeCaptured,
|
|
Command: Command{Name: "/bin/sh", Args: []string{"-c", "sleep 30"}},
|
|
WorkingDirectory: root, WorkspaceRoots: []string{root},
|
|
}
|
|
started, err := manager.Start(context.Background(), ProcessStart{Prepared: PreparedCommand{Command: command}, Request: request}, time.Millisecond)
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
stopped, err := manager.Continue(context.Background(), ProcessContinue{ProcessID: started.ProcessID, Interrupt: true, Wait: time.Second})
|
|
if err != nil {
|
|
t.Fatalf("Continue interrupt: %v", err)
|
|
}
|
|
if !stopped.Exited || !stopped.Interrupted {
|
|
t.Fatalf("interrupt result = %#v", stopped)
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerWriteInputUnknownProcess(t *testing.T) {
|
|
manager := NewProcessManager(ProcessManagerOptions{})
|
|
if err := manager.WriteInput(4242, []byte("x")); !errors.Is(err, ErrProcessNotFound) {
|
|
t.Fatalf("WriteInput unknown id = %v, want ErrProcessNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerWriteInputRejectsPipeProcess(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test command uses a POSIX shell")
|
|
}
|
|
root := t.TempDir()
|
|
manager := NewProcessManager(ProcessManagerOptions{})
|
|
command := exec.Command("/bin/sh", "-c", "sleep 30")
|
|
started, err := manager.Start(context.Background(), ProcessStart{
|
|
Prepared: PreparedCommand{Command: command}, Request: processManagerRequest(root, command),
|
|
}, time.Millisecond)
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
defer manager.Stop(started.ProcessID)
|
|
if started.TTY {
|
|
t.Fatal("pipe process reported a TTY")
|
|
}
|
|
if err := manager.WriteInput(started.ProcessID, []byte("x")); !errors.Is(err, ErrProcessStdinDisabled) {
|
|
t.Fatalf("WriteInput pipe process = %v, want ErrProcessStdinDisabled", err)
|
|
}
|
|
if err := manager.WriteInput(started.ProcessID, nil); err != nil {
|
|
t.Fatalf("WriteInput empty data = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerWriteInputDoesNotDrainPendingOutput(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("PTY sessions are only available on Linux")
|
|
}
|
|
root := t.TempDir()
|
|
manager := NewProcessManager(ProcessManagerOptions{})
|
|
command := exec.CommandContext(context.Background(), "cat")
|
|
started, err := manager.Start(context.Background(), ProcessStart{
|
|
Prepared: PreparedCommand{Command: command}, Request: processManagerRequest(root, command),
|
|
CommandText: "cat", TTY: true,
|
|
}, 50*time.Millisecond)
|
|
if err != nil {
|
|
t.Skipf("PTY transport unavailable: %v", err)
|
|
}
|
|
if !started.TTY {
|
|
t.Skip("PTY transport fell back to pipes")
|
|
}
|
|
defer manager.Stop(started.ProcessID)
|
|
|
|
if err := manager.WriteInput(started.ProcessID, []byte("hello\r")); err != nil {
|
|
t.Fatalf("WriteInput: %v", err)
|
|
}
|
|
// The PTY echoes input back into the output stream, so the rolling recent
|
|
// tail shows what was typed without anything being drained.
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
snapshot, ok := manager.Snapshot(started.ProcessID)
|
|
if ok && strings.Contains(snapshot.RecentOutput, "hello") {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("recent output never echoed the input: %q", snapshot.RecentOutput)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
// WriteInput must not have consumed the pending buffer: a write_stdin-style
|
|
// Continue still collects the echoed bytes.
|
|
continued, err := manager.Continue(context.Background(), ProcessContinue{
|
|
ProcessID: started.ProcessID, Wait: 200 * time.Millisecond,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Continue: %v", err)
|
|
}
|
|
if !strings.Contains(continued.Output, "hello") {
|
|
t.Fatalf("WriteInput drained the pending output: continued output = %q", continued.Output)
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerResizeInputUnknownProcess(t *testing.T) {
|
|
manager := NewProcessManager(ProcessManagerOptions{})
|
|
if err := manager.ResizeInput(4242, 100, 40); !errors.Is(err, ErrProcessNotFound) {
|
|
t.Fatalf("ResizeInput unknown id = %v, want ErrProcessNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerResizeInputUpdatesWindowSize(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("PTY sessions are only available on Linux")
|
|
}
|
|
root := t.TempDir()
|
|
manager := NewProcessManager(ProcessManagerOptions{})
|
|
command := exec.CommandContext(context.Background(), "/bin/sh", "-c", "sleep 0.3; stty size")
|
|
started, err := manager.Start(context.Background(), ProcessStart{
|
|
Prepared: PreparedCommand{Command: command}, Request: processManagerRequest(root, command),
|
|
CommandText: "stty size", TTY: true,
|
|
}, time.Millisecond)
|
|
if err != nil {
|
|
t.Skipf("PTY transport unavailable: %v", err)
|
|
}
|
|
if !started.TTY {
|
|
t.Skip("PTY transport fell back to pipes")
|
|
}
|
|
defer manager.Stop(started.ProcessID)
|
|
|
|
if err := manager.ResizeInput(started.ProcessID, 100, 40); err != nil {
|
|
t.Fatalf("ResizeInput: %v", err)
|
|
}
|
|
continued, err := manager.Continue(context.Background(), ProcessContinue{
|
|
ProcessID: started.ProcessID, Wait: 2 * time.Second,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Continue: %v", err)
|
|
}
|
|
if combined := started.Output + continued.Output; !strings.Contains(combined, "40 100") {
|
|
t.Fatalf("stty size output = %q, want %q", combined, "40 100")
|
|
}
|
|
}
|
|
|
|
func TestManagedProcessTerminateSkipsReapedProcess(t *testing.T) {
|
|
reaped := make(chan struct{})
|
|
close(reaped)
|
|
called := false
|
|
process := &managedProcess{
|
|
command: &exec.Cmd{Process: &os.Process{Pid: 4242}},
|
|
reaped: reaped,
|
|
kill: func(int) error { called = true; return nil },
|
|
}
|
|
|
|
process.terminate()
|
|
if called {
|
|
t.Fatal("terminate signaled an already-reaped process")
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerCancellationReportsInterrupted(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test command uses a POSIX shell")
|
|
}
|
|
root := t.TempDir()
|
|
manager := NewProcessManager(ProcessManagerOptions{})
|
|
command := exec.Command("/bin/sh", "-c", "sleep 30")
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
result, err := manager.Start(ctx, ProcessStart{
|
|
Prepared: PreparedCommand{Command: command},
|
|
Request: processManagerRequest(root, command),
|
|
}, time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if !result.Exited || !result.Interrupted {
|
|
t.Fatalf("cancelled result = %#v, want exited and interrupted", result)
|
|
}
|
|
}
|
|
|
|
func TestProcessManagerObservesChangesMadeDuringTransportStart(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test command uses a POSIX shell")
|
|
}
|
|
root := t.TempDir()
|
|
target := filepath.Join(root, "created.txt")
|
|
manager := NewProcessManager(ProcessManagerOptions{})
|
|
manager.startTransport = func(command *exec.Cmd, output io.Writer, tty bool) (io.WriteCloser, bool, func(), error) {
|
|
if err := os.WriteFile(target, []byte("created"), 0o600); err != nil {
|
|
return nil, false, nil, err
|
|
}
|
|
return startProcessTransport(command, output, tty)
|
|
}
|
|
command := exec.Command("/bin/sh", "-c", "true")
|
|
|
|
result, err := manager.Start(context.Background(), ProcessStart{
|
|
Prepared: PreparedCommand{Command: command},
|
|
Request: processManagerRequest(root, command),
|
|
}, time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if len(result.Changes) != 1 || result.Changes[0].Path != "created.txt" || result.Changes[0].Kind != ChangeCreated {
|
|
t.Fatalf("changes = %#v, want created.txt created", result.Changes)
|
|
}
|
|
}
|
|
|
|
func TestProcessWaitsClampToEffectiveBounds(t *testing.T) {
|
|
if got := clampInitialProcessWait(time.Minute); got != maxInteractiveYield {
|
|
t.Fatalf("initial upper clamp = %v, want %v", got, maxInteractiveYield)
|
|
}
|
|
if got := clampInitialProcessWait(time.Millisecond); got != time.Millisecond {
|
|
t.Fatalf("initial short wait = %v, want caller value", got)
|
|
}
|
|
if got := clampContinuationWait(time.Hour, true); got != maxEmptyPollYield {
|
|
t.Fatalf("empty poll upper clamp = %v, want %v", got, maxEmptyPollYield)
|
|
}
|
|
if got := clampContinuationWait(time.Hour, false); got != maxInteractiveYield {
|
|
t.Fatalf("stdin upper clamp = %v, want %v", got, maxInteractiveYield)
|
|
}
|
|
}
|
|
|
|
func TestProcessOutputBufferCapsUndrainedData(t *testing.T) {
|
|
buffer := newProcessOutputBuffer()
|
|
chunk := []byte(strings.Repeat("x", 1024))
|
|
for i := 0; i < maxPendingOutputBytes/len(chunk)+10; i++ {
|
|
if _, err := buffer.Write(chunk); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
}
|
|
|
|
buffer.mu.Lock()
|
|
dataLen := len(buffer.data)
|
|
buffer.mu.Unlock()
|
|
if dataLen > maxPendingOutputBytes {
|
|
t.Fatalf("undrained buffer grew to %d bytes, want <= %d", dataLen, maxPendingOutputBytes)
|
|
}
|
|
if got := buffer.drain(); !strings.HasSuffix(string(got), string(chunk)) {
|
|
t.Fatal("drained output should retain the newest bytes")
|
|
}
|
|
if !buffer.peekTruncated() || !buffer.consumeTruncated() || buffer.consumeTruncated() {
|
|
t.Fatal("truncation state was not preserved and consumed exactly once")
|
|
}
|
|
}
|
|
|
|
func TestManagedProcessCollectRespectsDeadlineUnderContinuousOutput(t *testing.T) {
|
|
process := &managedProcess{output: newProcessOutputBuffer(), done: make(chan struct{})}
|
|
stop := make(chan struct{})
|
|
var writers sync.WaitGroup
|
|
for i := 0; i < 8; i++ {
|
|
writers.Add(1)
|
|
go func() {
|
|
defer writers.Done()
|
|
chunk := []byte(strings.Repeat("x", 256))
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
_, _ = process.output.Write(chunk)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
t.Cleanup(func() { close(stop); writers.Wait() })
|
|
|
|
wait := 200 * time.Millisecond
|
|
started := time.Now()
|
|
_, _ = process.collect(context.Background(), wait)
|
|
if elapsed := time.Since(started); elapsed > 5*wait {
|
|
t.Fatalf("collect took %v under continuous output, want close to %v", elapsed, wait)
|
|
}
|
|
}
|
|
|
|
func TestManagedProcessCollectCapsCumulativeOutput(t *testing.T) {
|
|
process := &managedProcess{output: newProcessOutputBuffer(), done: make(chan struct{})}
|
|
chunk := []byte(strings.Repeat("x", maxPendingOutputBytes/2))
|
|
go func() {
|
|
for i := 0; i < 5; i++ {
|
|
_, _ = process.output.Write(chunk)
|
|
for {
|
|
process.output.mu.Lock()
|
|
drained := len(process.output.data) == 0
|
|
process.output.mu.Unlock()
|
|
if drained {
|
|
break
|
|
}
|
|
runtime.Gosched()
|
|
}
|
|
}
|
|
close(process.done)
|
|
}()
|
|
|
|
output, truncated := process.collect(context.Background(), time.Second)
|
|
if len(output) > maxPendingOutputBytes {
|
|
t.Fatalf("collected %d bytes, want <= %d", len(output), maxPendingOutputBytes)
|
|
}
|
|
if !truncated {
|
|
t.Fatal("cumulative output cap must report truncation")
|
|
}
|
|
}
|
|
|
|
func TestProcessPruningDoesNotRaceTouch(t *testing.T) {
|
|
manager := NewProcessManager(ProcessManagerOptions{})
|
|
for id := 1000; id < 1012; id++ {
|
|
manager.processes[id] = &managedProcess{id: id, lastUsedAt: time.Now(), done: make(chan struct{})}
|
|
}
|
|
target := manager.processes[1000]
|
|
stop := make(chan struct{})
|
|
var writer sync.WaitGroup
|
|
writer.Add(1)
|
|
go func() {
|
|
defer writer.Done()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
target.touch()
|
|
}
|
|
}
|
|
}()
|
|
for i := 0; i < 2000; i++ {
|
|
manager.mu.Lock()
|
|
_ = manager.processToPruneLocked()
|
|
manager.mu.Unlock()
|
|
}
|
|
close(stop)
|
|
writer.Wait()
|
|
}
|