mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
Merge branch 'main' into pr/350
This commit is contained in:
+17
-2
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/reflection"
|
||||
|
||||
"github.com/memohai/memoh/internal/logger"
|
||||
@@ -49,7 +50,7 @@ func initDataDir() {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(dst, data, fs.FileMode(0o644)); err != nil {
|
||||
if err := os.WriteFile(dst, data, fs.FileMode(0o644)); err != nil { //nolint:gosec // G703: dst is built from filepath.Join(defaultWorkDir, e.Name()) where e comes from os.ReadDir
|
||||
logger.Warn("failed to seed template", slog.String("file", e.Name()), slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
@@ -91,7 +92,21 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
srv := grpc.NewServer()
|
||||
srv := grpc.NewServer(
|
||||
grpc.MaxRecvMsgSize(16*1024*1024),
|
||||
grpc.MaxSendMsgSize(16*1024*1024),
|
||||
grpc.KeepaliveParams(keepalive.ServerParameters{
|
||||
MaxConnectionIdle: 5 * time.Minute,
|
||||
MaxConnectionAge: 30 * time.Minute,
|
||||
MaxConnectionAgeGrace: 10 * time.Second,
|
||||
Time: 60 * time.Second,
|
||||
Timeout: 15 * time.Second,
|
||||
}),
|
||||
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
|
||||
MinTime: 10 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}),
|
||||
)
|
||||
pb.RegisterContainerServiceServer(srv, &containerServer{})
|
||||
reflection.Register(srv)
|
||||
|
||||
|
||||
+46
-19
@@ -24,14 +24,15 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
readMaxLines = 200
|
||||
readMaxBytes = 5120
|
||||
readMaxLineLen = 1000
|
||||
listMaxEntries = 200
|
||||
binaryProbeBytes = 8 * 1024
|
||||
rawChunkSize = 64 * 1024
|
||||
defaultWorkDir = "/data"
|
||||
defaultTimeout = 30
|
||||
readMaxLines = 2000
|
||||
readMaxBytes = 0 // 0 = no byte limit (line count only)
|
||||
readMaxLineLen = 0 // 0 = no per-line truncation
|
||||
listMaxEntries = 200
|
||||
binaryProbeBytes = 8 * 1024
|
||||
rawChunkSize = 64 * 1024
|
||||
defaultWorkDir = "/data"
|
||||
defaultTimeout = 30
|
||||
defaultPTYTimeout = 5 * 60 // 5 minutes max for PTY sessions (agent tool calls)
|
||||
)
|
||||
|
||||
type containerServer struct {
|
||||
@@ -89,12 +90,12 @@ func (*containerServer) ReadFile(_ context.Context, req *pb.ReadFileRequest) (*p
|
||||
}
|
||||
|
||||
line := scanner.Text()
|
||||
if utf8.RuneCountInString(line) > readMaxLineLen {
|
||||
if readMaxLineLen > 0 && utf8.RuneCountInString(line) > readMaxLineLen {
|
||||
line = truncateRunes(line, readMaxLineLen) + "..."
|
||||
}
|
||||
|
||||
entry := line + "\n"
|
||||
if bytesWritten+len(entry) > readMaxBytes {
|
||||
if readMaxBytes > 0 && bytesWritten+len(entry) > readMaxBytes {
|
||||
break
|
||||
}
|
||||
out.WriteString(entry)
|
||||
@@ -288,11 +289,18 @@ func execPTY(stream pb.ContainerService_ExecServer, firstMsg *pb.ExecInput) erro
|
||||
workDir = defaultWorkDir
|
||||
}
|
||||
|
||||
timeout := int(firstMsg.GetTimeoutSeconds())
|
||||
if timeout <= 0 {
|
||||
timeout = defaultPTYTimeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(stream.Context(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if isBarePath(command) {
|
||||
cmd = exec.CommandContext(stream.Context(), command) //nolint:gosec // G204: intentional
|
||||
cmd = exec.CommandContext(ctx, command) //nolint:gosec // G204: intentional
|
||||
} else {
|
||||
cmd = exec.CommandContext(stream.Context(), "/bin/sh", "-c", command) //nolint:gosec // G204: intentional
|
||||
cmd = exec.CommandContext(ctx, "/bin/sh", "-c", command) //nolint:gosec // G204: intentional
|
||||
}
|
||||
cmd.Dir = workDir
|
||||
cmd.Env = append(os.Environ(), firstMsg.GetEnv()...)
|
||||
@@ -361,13 +369,14 @@ func execPipe(stream pb.ContainerService_ExecServer, firstMsg *pb.ExecInput) err
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
|
||||
// Keep non-PTY execs alive across transport cancellation so a dropped
|
||||
// stream does not rewrite a successful command into exit -1. The timeout
|
||||
// still bounds command lifetime, and stream shutdown still closes stdin.
|
||||
ctx, cancel := context.WithTimeout(context.WithoutCancel(stream.Context()), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
// Process context is independent of the gRPC stream so the process keeps
|
||||
// running even if the stream is cancelled (e.g. background tasks whose client
|
||||
// disconnects or whose stream context dies after the process completes).
|
||||
// Only the process-level timeout kills the process, not stream death.
|
||||
procCtx, procCancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer procCancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command) //nolint:gosec // G204: MCP exec tool intentionally executes agent-issued shell commands inside the container
|
||||
cmd := exec.CommandContext(procCtx, "/bin/sh", "-c", command) //nolint:gosec // G204: MCP exec tool intentionally executes agent-issued shell commands inside the container
|
||||
cmd.Dir = workDir
|
||||
if len(firstMsg.GetEnv()) > 0 {
|
||||
cmd.Env = append(os.Environ(), firstMsg.GetEnv()...)
|
||||
@@ -391,6 +400,19 @@ func execPipe(stream pb.ContainerService_ExecServer, firstMsg *pb.ExecInput) err
|
||||
return status.Errorf(codes.Internal, "start: %v", err)
|
||||
}
|
||||
|
||||
// Close pipes when EITHER the process finishes (procCtx done) OR the gRPC
|
||||
// stream dies (stream.Context done). Closing unblocks streamPipe's Read so
|
||||
// the goroutines can exit. We do NOT cancel procCtx on stream death — the
|
||||
// process keeps running so background tasks survive client disconnects.
|
||||
go func() {
|
||||
select {
|
||||
case <-procCtx.Done():
|
||||
case <-stream.Context().Done():
|
||||
}
|
||||
_ = stdoutPipe.Close()
|
||||
_ = stderrPipe.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
msg, recvErr := stream.Recv()
|
||||
@@ -423,10 +445,15 @@ func execPipe(stream pb.ContainerService_ExecServer, firstMsg *pb.ExecInput) err
|
||||
}
|
||||
}
|
||||
|
||||
return stream.Send(&pb.ExecOutput{
|
||||
// Send exit code to the client. If the stream is already gone (e.g. the
|
||||
// client is a background task manager that got a stream error when the
|
||||
// process completed), the send will fail but we return nil so the gRPC
|
||||
// handler does not propagate a spurious "context canceled" error status.
|
||||
_ = stream.Send(&pb.ExecOutput{
|
||||
Stream: pb.ExecOutput_EXIT,
|
||||
ExitCode: exitCode,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*containerServer) ReadRaw(req *pb.ReadRawRequest, stream pb.ContainerService_ReadRawServer) error {
|
||||
|
||||
Reference in New Issue
Block a user