源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
package agentruntime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func TestProbeCodexAppServerCompletesFormalInitialize(t *testing.T) {
|
||||
t.Parallel()
|
||||
transport := newScriptedAppServerTransport()
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
HandshakeTimeout: time.Second,
|
||||
})
|
||||
if !result.CommandStarted || !result.ProtocolReady {
|
||||
t.Fatalf("probe = %#v, want command and protocol ready", result)
|
||||
}
|
||||
if got := len(appServerRequestParamsList(t, transport.conn, appServerMethodInitialize)); got != 1 {
|
||||
t.Fatalf("initialize calls = %d, want 1", got)
|
||||
}
|
||||
if got := len(appServerRequestParamsList(t, transport.conn, appServerMethodInitialized)); got != 1 {
|
||||
t.Fatalf("initialized notifications = %d, want 1", got)
|
||||
}
|
||||
if result.CommandStarted != result.ProtocolReady && result.ProtocolReady {
|
||||
t.Fatalf("protocol-ready invariant violated: %#v", result)
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, transport.conn)
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerStartsExactlyOneProcess(t *testing.T) {
|
||||
t.Parallel()
|
||||
transport := newScriptedAppServerTransport()
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
})
|
||||
if !result.ProtocolReady {
|
||||
t.Fatalf("probe = %#v, want protocol ready", result)
|
||||
}
|
||||
transport.mu.Lock()
|
||||
starts := len(transport.specs)
|
||||
transport.mu.Unlock()
|
||||
if starts != 1 {
|
||||
t.Fatalf("Start calls = %d, want 1", starts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerReadsAuthenticatedAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
transport := newScriptedAppServerTransport()
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
HandshakeTimeout: time.Second, ReadAccount: true,
|
||||
})
|
||||
if !result.ProtocolReady || !result.AccountRead ||
|
||||
result.AccountState != CodexAppServerAccountAuthenticated {
|
||||
t.Fatalf("probe = %#v, want authenticated account/read", result)
|
||||
}
|
||||
if result.AuthMethod != "chatgpt" || result.AccountLabel != "dev@example.com" {
|
||||
t.Fatalf("account identity = %q / %q, want chatgpt / dev@example.com", result.AuthMethod, result.AccountLabel)
|
||||
}
|
||||
if got := len(appServerRequestParamsList(t, transport.conn, appServerMethodAccountRead)); got != 1 {
|
||||
t.Fatalf("account/read calls = %d, want 1", got)
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, transport.conn)
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerReadsRequiredAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
transport := newScriptedAppServerTransport()
|
||||
transport.server.requiresAuth = true
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
HandshakeTimeout: time.Second, ReadAccount: true,
|
||||
})
|
||||
if !result.ProtocolReady || !result.AccountRead ||
|
||||
result.AccountState != CodexAppServerAccountRequired {
|
||||
t.Fatalf("probe = %#v, want required account/read", result)
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, transport.conn)
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerDoesNotAuthenticateAccountReadFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
transport := newScriptedAppServerTransport()
|
||||
transport.server.accountReadError = true
|
||||
transport.server.accountReadErrorCode = -32600
|
||||
transport.server.accountReadErrorMessage = "plan type is required for chatgpt authentication"
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
HandshakeTimeout: time.Second, ReadAccount: true,
|
||||
})
|
||||
if !result.ProtocolReady || result.AccountRead ||
|
||||
result.AccountState != CodexAppServerAccountUnknown {
|
||||
t.Fatalf("probe = %#v, want unknown account after account/read failure", result)
|
||||
}
|
||||
if result.AccountCategory != CodexProbeProtocolFailure {
|
||||
t.Fatalf("account category = %q, want %q", result.AccountCategory, CodexProbeProtocolFailure)
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, transport.conn)
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerReadsProviderRateLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
transport := newScriptedAppServerTransport()
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
HandshakeTimeout: time.Second, ReadRateLimits: true,
|
||||
})
|
||||
if !result.ProtocolReady || !result.RateLimitsRead || result.Category != "" {
|
||||
t.Fatalf("probe = %#v, want successful rate limits read", result)
|
||||
}
|
||||
rateLimits, ok := result.RateLimits["rateLimits"].(map[string]any)
|
||||
if !ok || rateLimits["primary"] == nil {
|
||||
t.Fatalf("rate limits payload = %#v, want primary window", result.RateLimits)
|
||||
}
|
||||
if got := len(appServerRequestParamsList(t, transport.conn, appServerMethodRateLimitsRead)); got != 1 {
|
||||
t.Fatalf("account/rateLimits/read calls = %d, want 1", got)
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, transport.conn)
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerReturnsRateLimitsAuthenticationFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
transport := newScriptedAppServerTransport()
|
||||
transport.server.rateLimitsReadError = true
|
||||
transport.server.rateLimitsReadErrorCode = -32001
|
||||
transport.server.rateLimitsReadErrorMessage = "401 Unauthorized: refresh token was revoked"
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
HandshakeTimeout: time.Second, ReadRateLimits: true,
|
||||
})
|
||||
if !result.ProtocolReady || result.RateLimitsRead ||
|
||||
result.RateLimitsCategory != CodexProbeProtocolFailure ||
|
||||
!strings.Contains(result.Message, "refresh token was revoked") {
|
||||
t.Fatalf("probe = %#v, want rate limits authentication failure", result)
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, transport.conn)
|
||||
}
|
||||
|
||||
func TestParseCodexProbeAccountRejectsIncompleteResponses(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, raw := range []string{
|
||||
`{}`,
|
||||
`{"requiresOpenaiAuth":false,"account":{}}`,
|
||||
`{"requiresOpenaiAuth":false,"account":{"type":"chatgpt","email":"dev@example.com"}}`,
|
||||
`{"requiresOpenaiAuth":false,"account":{"type":"unexpected"}}`,
|
||||
} {
|
||||
if account, ok := parseCodexProbeAccount(json.RawMessage(raw)); ok {
|
||||
t.Fatalf("parseCodexProbeAccount(%s) = %#v, true; want invalid", raw, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerTimesOutDuringStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
transport := &blockingStartProbeTransport{entered: make(chan struct{})}
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport, StartupTimeout: 20 * time.Millisecond,
|
||||
})
|
||||
if result.CommandStarted || result.ProtocolReady || result.Category != CodexProbeStartupTimeout {
|
||||
t.Fatalf("probe = %#v, want startup timeout before command start", result)
|
||||
}
|
||||
if got := transport.calls.Load(); got != 1 {
|
||||
t.Fatalf("Start calls = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerCancellationDuringHandshake(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
connection := newScriptedAppServerConnection()
|
||||
transport := &cancellationProbeTransport{conn: connection, initializeSent: make(chan struct{})}
|
||||
done := make(chan CodexAppServerProbeResult, 1)
|
||||
go func() {
|
||||
done <- ProbeCodexAppServer(ctx, CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport, HandshakeTimeout: time.Second,
|
||||
})
|
||||
}()
|
||||
select {
|
||||
case <-transport.initializeSent:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("initialize was not sent")
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case result := <-done:
|
||||
if !result.CommandStarted || result.ProtocolReady || result.Category != CodexProbeCanceled {
|
||||
t.Fatalf("probe = %#v, want canceled handshake", result)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("probe did not return after cancellation")
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, connection)
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerBoundsStderr(t *testing.T) {
|
||||
t.Parallel()
|
||||
connection := newNoisyProbeConnection(10 << 20)
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: noisyProbeTransport{conn: connection},
|
||||
HandshakeTimeout: time.Second,
|
||||
})
|
||||
if len(result.StderrTail) > acpClientOutputTailLimit {
|
||||
t.Fatalf("stderr result = %#v, want bounded tail", result)
|
||||
}
|
||||
if !utf8.ValidString(result.StderrTail) {
|
||||
t.Fatalf("stderr tail is not UTF-8: %q", result.StderrTail)
|
||||
}
|
||||
select {
|
||||
case <-connection.producerDone:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("stderr producer did not stop")
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, connection.scriptedAppServerConnection)
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerReapsLocalHelperProcess(t *testing.T) {
|
||||
pidPath := filepath.Join(t.TempDir(), "probe-helper.pid")
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{os.Args[0], "-test.run=^TestProbeCodexAppServerHelperProcess$", "--"},
|
||||
Env: []string{"TUTTI_CODEX_PROBE_HELPER=1", "TUTTI_CODEX_PROBE_PID_PATH=" + pidPath},
|
||||
HandshakeTimeout: 100 * time.Millisecond,
|
||||
ShutdownTimeout: 100 * time.Millisecond,
|
||||
})
|
||||
if result.ProtocolReady || result.Category != CodexProbeHandshakeTimeout {
|
||||
t.Fatalf("probe = %#v, want bounded local-helper timeout", result)
|
||||
}
|
||||
if _, err := os.ReadFile(pidPath); err != nil {
|
||||
t.Fatalf("helper did not start or write pid: %v", err)
|
||||
}
|
||||
// Probe only returns after localProcessConnection.Close has waited for the
|
||||
// child. This is a real ProcessTransport integration test, not a fake
|
||||
// connection Close assertion.
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerHelperProcess(_ *testing.T) {
|
||||
if os.Getenv("TUTTI_CODEX_PROBE_HELPER") != "1" {
|
||||
return
|
||||
}
|
||||
if pidPath := os.Getenv("TUTTI_CODEX_PROBE_PID_PATH"); pidPath != "" {
|
||||
if err := os.WriteFile(pidPath, []byte(fmt.Sprint(os.Getpid())), 0o600); err != nil {
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
_, _ = os.Stderr.Write(make([]byte, 10<<20))
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerTimesOutWithoutInitializeResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
transport := &silentProbeTransport{conn: newScriptedAppServerConnection()}
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
HandshakeTimeout: 20 * time.Millisecond,
|
||||
})
|
||||
if !result.CommandStarted || result.ProtocolReady || result.Category != CodexProbeHandshakeTimeout {
|
||||
t.Fatalf("probe = %#v, want command pass plus handshake timeout", result)
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, transport.conn)
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerRejectsInitializeErrorAndInitializedSendFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
transport ProcessTransport
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "initialize error response",
|
||||
transport: &initializeErrorProbeTransport{conn: newScriptedAppServerConnection()},
|
||||
want: CodexProbeProtocolFailure,
|
||||
},
|
||||
{
|
||||
name: "initialized send failure",
|
||||
transport: &initializedSendFailureProbeTransport{conn: newScriptedAppServerConnection()},
|
||||
want: CodexProbeProtocolFailure,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: test.transport, HandshakeTimeout: time.Second,
|
||||
})
|
||||
if !result.CommandStarted || result.ProtocolReady || result.Category != test.want {
|
||||
t.Fatalf("probe = %#v, want %s", result, test.want)
|
||||
}
|
||||
var connection *scriptedAppServerConnection
|
||||
switch transport := test.transport.(type) {
|
||||
case *initializeErrorProbeTransport:
|
||||
connection = transport.conn
|
||||
case *initializedSendFailureProbeTransport:
|
||||
connection = transport.conn
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, connection)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerRejectsMalformedResponseAndCloses(t *testing.T) {
|
||||
t.Parallel()
|
||||
connection := newScriptedAppServerConnection()
|
||||
transport := &invalidProbeTransport{conn: connection}
|
||||
result := ProbeCodexAppServer(context.Background(), CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
HandshakeTimeout: time.Second,
|
||||
})
|
||||
if !result.CommandStarted || result.ProtocolReady || result.Category != CodexProbeInvalidResponse {
|
||||
t.Fatalf("probe = %#v, want malformed protocol failure", result)
|
||||
}
|
||||
assertScriptedProbeConnectionClosed(t, connection)
|
||||
}
|
||||
|
||||
func TestProbeCodexAppServerCancellationClosesTransport(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
transport := &silentProbeTransport{conn: newScriptedAppServerConnection()}
|
||||
result := ProbeCodexAppServer(ctx, CodexAppServerProbeInput{
|
||||
Command: []string{"codex", "app-server"}, Transport: transport,
|
||||
HandshakeTimeout: time.Second,
|
||||
})
|
||||
if result.CommandStarted || result.ProtocolReady || result.Category != CodexProbeCanceled {
|
||||
t.Fatalf("probe = %#v, want canceled startup", result)
|
||||
}
|
||||
}
|
||||
|
||||
type silentProbeTransport struct{ conn *scriptedAppServerConnection }
|
||||
|
||||
func (t *silentProbeTransport) Start(_ context.Context, _ ProcessSpec) (ProcessConnection, error) {
|
||||
return &silentProbeConnection{scriptedAppServerConnection: t.conn}, nil
|
||||
}
|
||||
|
||||
type silentProbeConnection struct{ *scriptedAppServerConnection }
|
||||
|
||||
func (*silentProbeConnection) Send([]byte) error { return nil }
|
||||
|
||||
type invalidProbeTransport struct{ conn *scriptedAppServerConnection }
|
||||
|
||||
func (t *invalidProbeTransport) Start(_ context.Context, _ ProcessSpec) (ProcessConnection, error) {
|
||||
return &invalidProbeConnection{scriptedAppServerConnection: t.conn}, nil
|
||||
}
|
||||
|
||||
type invalidProbeConnection struct{ *scriptedAppServerConnection }
|
||||
|
||||
func (c *invalidProbeConnection) Send([]byte) error {
|
||||
for range acpClientStdoutProtocolErrorLimit {
|
||||
c.recv <- ProcessFrame{Stdout: []byte("not-json\n")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type initializeErrorProbeTransport struct{ conn *scriptedAppServerConnection }
|
||||
|
||||
func (t *initializeErrorProbeTransport) Start(context.Context, ProcessSpec) (ProcessConnection, error) {
|
||||
return &initializeErrorProbeConnection{scriptedAppServerConnection: t.conn}, nil
|
||||
}
|
||||
|
||||
type initializeErrorProbeConnection struct{ *scriptedAppServerConnection }
|
||||
|
||||
func (c *initializeErrorProbeConnection) Send(data []byte) error {
|
||||
for _, line := range acpScanLines(data) {
|
||||
var request struct {
|
||||
ID json.RawMessage `json:"id"`
|
||||
}
|
||||
_ = json.Unmarshal([]byte(line), &request)
|
||||
c.sendJSON(map[string]any{"id": request.ID, "error": map[string]any{"code": -32000, "message": "initialize rejected"}})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type initializedSendFailureProbeTransport struct{ conn *scriptedAppServerConnection }
|
||||
|
||||
func (t *initializedSendFailureProbeTransport) Start(context.Context, ProcessSpec) (ProcessConnection, error) {
|
||||
return &initializedSendFailureProbeConnection{scriptedAppServerConnection: t.conn}, nil
|
||||
}
|
||||
|
||||
type initializedSendFailureProbeConnection struct {
|
||||
*scriptedAppServerConnection
|
||||
calls int
|
||||
}
|
||||
|
||||
func (c *initializedSendFailureProbeConnection) Send(data []byte) error {
|
||||
c.calls++
|
||||
if c.calls == 2 {
|
||||
return fmt.Errorf("initialized write failed")
|
||||
}
|
||||
return c.scriptedAppServerConnection.Send(data)
|
||||
}
|
||||
|
||||
func assertScriptedProbeConnectionClosed(t *testing.T, connection *scriptedAppServerConnection) {
|
||||
t.Helper()
|
||||
connection.mu.Lock()
|
||||
closed := connection.closeCount
|
||||
connection.mu.Unlock()
|
||||
if closed != 1 {
|
||||
t.Fatalf("connection close count = %d, want 1", closed)
|
||||
}
|
||||
}
|
||||
|
||||
type blockingStartProbeTransport struct {
|
||||
calls atomic.Int32
|
||||
entered chan struct{}
|
||||
}
|
||||
|
||||
func (t *blockingStartProbeTransport) Start(ctx context.Context, _ ProcessSpec) (ProcessConnection, error) {
|
||||
t.calls.Add(1)
|
||||
select {
|
||||
case t.entered <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
type cancellationProbeTransport struct {
|
||||
conn *scriptedAppServerConnection
|
||||
initializeSent chan struct{}
|
||||
}
|
||||
|
||||
func (t *cancellationProbeTransport) Start(context.Context, ProcessSpec) (ProcessConnection, error) {
|
||||
return &cancellationProbeConnection{scriptedAppServerConnection: t.conn, initializeSent: t.initializeSent}, nil
|
||||
}
|
||||
|
||||
type cancellationProbeConnection struct {
|
||||
*scriptedAppServerConnection
|
||||
initializeSent chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (c *cancellationProbeConnection) Send(_ []byte) error {
|
||||
c.once.Do(func() { close(c.initializeSent) })
|
||||
return nil
|
||||
}
|
||||
|
||||
type noisyProbeTransport struct{ conn *noisyProbeConnection }
|
||||
|
||||
func (t noisyProbeTransport) Start(context.Context, ProcessSpec) (ProcessConnection, error) {
|
||||
return t.conn, nil
|
||||
}
|
||||
|
||||
type noisyProbeConnection struct {
|
||||
*scriptedAppServerConnection
|
||||
bytesToWrite int
|
||||
producerDone chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func newNoisyProbeConnection(bytesToWrite int) *noisyProbeConnection {
|
||||
return &noisyProbeConnection{scriptedAppServerConnection: newScriptedAppServerConnection(), bytesToWrite: bytesToWrite, producerDone: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (c *noisyProbeConnection) Send([]byte) error {
|
||||
c.once.Do(func() {
|
||||
go func() {
|
||||
defer close(c.producerDone)
|
||||
chunk := make([]byte, 64<<10)
|
||||
for written := 0; written < c.bytesToWrite; written += len(chunk) {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return
|
||||
case c.recv <- ProcessFrame{Stderr: chunk}:
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user