mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
CI / build (push) Failing after 33s
Go / generate (push) Failing after 46s
The git ceiling named the workspace, which git only applies to a proper ancestor of the directory it starts from, so a request with no dir found, read and wrote a repository above the workspace. The ceiling is now the workspace's parent, which refuses from the root and from any directory under it. patch staged by action id, so two changes naming one file collided and the second failed with the first's bytes already in place, and a delete that could not happen left earlier renames behind. Every change now stages under a random name, the file that was there is kept on a second link until every move has landed, and a move that fails is put back. A diff that names one file twice, gives two names for one file, would delete a directory, or renames is refused. A zero-length packet is a packet on SOCK_SEQPACKET, and reading it as end of file ended the daemon for good. POLLRDHUP tells the two apart. Also: a listing fills a page to what a frame holds instead of promising 4096 entries no frame can carry; write and patch resolve the parent with openat2 on the path as written, so one string cannot mean two places; a refused request is not recorded, so its id is free, while an action that ran is recorded whatever came of it; a staged name is random, so a workspace file is never unlinked to make room; children start from a declared environment instead of the daemon's; write reports the mode on disk and keeps the mode a file had; mode means permission bits in stat as well as read and list; a read past the end is refused; an oversize refusal names the op it refused; the ledger is bounded by bytes as well as count; the diff path is the reading both header lines agree on; the build constraint sits above the package doc; and BUILD embeds a library, which is how the repo builds a main package with tests. Co-authored-by: Hanzo Dev <[email protected]>
171 lines
4.6 KiB
Go
171 lines
4.6 KiB
Go
// Copyright 2026 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// outMax bounds each of a child's two output streams in one reply. Past it
|
|
// the reply says how much was dropped; it never pretends the output is whole.
|
|
const outMax = 48 << 10
|
|
|
|
// shell is what a command string is handed to.
|
|
var shell = []string{"/bin/sh", "-c"}
|
|
|
|
// preset is the environment a child starts from when a request declares none.
|
|
// It is a stated list, not whatever the daemon happens to hold, so nothing the
|
|
// host handed execd reaches a child unasked.
|
|
var preset = []string{
|
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
"LANG=C.UTF-8",
|
|
"TERM=xterm-256color",
|
|
}
|
|
|
|
// environ settles a child's environment: what the request declared, or the
|
|
// preset, and then extra.
|
|
func environ(r *req, extra ...string) []string {
|
|
env := preset
|
|
if len(r.env) > 0 {
|
|
env = r.env
|
|
}
|
|
out := make([]string, 0, len(env)+len(extra))
|
|
return append(append(out, env...), extra...)
|
|
}
|
|
|
|
func (d *daemon) exec(r *req) *rep {
|
|
argv, err := argvOf(r)
|
|
if err != nil {
|
|
return fail(r, err)
|
|
}
|
|
return d.run(r, argv, nil)
|
|
}
|
|
|
|
// argvOf settles what a request asked to run: argv, or a command string for
|
|
// a shell. Both, or neither, is refused.
|
|
func argvOf(r *req) ([]string, error) {
|
|
switch {
|
|
case r.command != "" && len(r.argv) > 0:
|
|
return nil, errors.New("argv and command are both set; give one")
|
|
case r.command != "":
|
|
return append(append([]string{}, shell...), r.command), nil
|
|
case len(r.argv) > 0:
|
|
return r.argv, nil
|
|
}
|
|
return nil, errors.New("neither argv nor command is set")
|
|
}
|
|
|
|
// run starts a child in the workspace and waits for it. extra is appended to
|
|
// the child's environment after the request's own.
|
|
func (d *daemon) run(r *req, argv []string, extra []string) *rep {
|
|
dir, err := d.root.abs(r.dir)
|
|
if err != nil {
|
|
return fail(r, err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
stop := func() {}
|
|
if r.timeout > 0 {
|
|
ctx, stop = context.WithTimeout(ctx, time.Duration(r.timeout)*time.Millisecond)
|
|
}
|
|
defer stop()
|
|
|
|
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
|
|
cmd.Dir = dir
|
|
cmd.Env = environ(r, extra...)
|
|
if len(r.data) > 0 {
|
|
cmd.Stdin = bytes.NewReader(r.data)
|
|
}
|
|
out := &capped{max: outMax}
|
|
log := &capped{max: outMax}
|
|
cmd.Stdout, cmd.Stderr = out, log
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
cmd.Cancel = func() error {
|
|
if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != syscall.ESRCH {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
cmd.WaitDelay = time.Second
|
|
|
|
werr := cmd.Run()
|
|
reply := &rep{data: out.buf, log: log.buf}
|
|
|
|
var exit *exec.ExitError
|
|
switch {
|
|
case ctx.Err() == context.DeadlineExceeded:
|
|
// The deadline is the reason, whatever shape the wait error took.
|
|
reply.exit = -1
|
|
reply.err = fmt.Sprintf("no answer within %dms", r.timeout)
|
|
case werr == nil:
|
|
case errors.As(werr, &exit):
|
|
reply.exit = int32(exit.ExitCode())
|
|
if st, ok := exit.Sys().(syscall.WaitStatus); ok && st.Signaled() {
|
|
reply.err = fmt.Sprintf("killed by %s", st.Signal())
|
|
}
|
|
case cmd.Process == nil:
|
|
// It never started, so nothing happened and asking again is free.
|
|
return fail(r, fmt.Errorf("%s: %w", argv[0], werr))
|
|
default:
|
|
// It ran; whatever went wrong after that is its outcome.
|
|
reply.exit = -1
|
|
reply.err = fmt.Sprintf("%s: %v", argv[0], werr)
|
|
}
|
|
if note := strings.TrimSpace(out.note("stdout") + " " + log.note("stderr")); note != "" {
|
|
if reply.err != "" {
|
|
reply.err += "; "
|
|
}
|
|
reply.err += note
|
|
}
|
|
return reply
|
|
}
|
|
|
|
// capped collects up to max bytes and counts what it had to drop.
|
|
type capped struct {
|
|
buf []byte
|
|
max int
|
|
over int
|
|
}
|
|
|
|
func (c *capped) Write(p []byte) (int, error) {
|
|
room := c.max - len(c.buf)
|
|
if room > len(p) {
|
|
room = len(p)
|
|
}
|
|
if room > 0 {
|
|
c.buf = append(c.buf, p[:room]...)
|
|
} else {
|
|
room = 0
|
|
}
|
|
c.over += len(p) - room
|
|
return len(p), nil
|
|
}
|
|
|
|
func (c *capped) note(name string) string {
|
|
if c.over == 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%s kept %d bytes and dropped %d; redirect it to a file", name, len(c.buf), c.over)
|
|
}
|