зеркало из https://github.com/golang/build.git
1964 строки
55 KiB
Go
1964 строки
55 KiB
Go
// Copyright 2014 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// The buildlet is an HTTP server that untars content to disk and runs
|
|
// commands it has untarred, streaming their output back over HTTP.
|
|
// It is part of Go's continuous build system.
|
|
//
|
|
// This program intentionally allows remote code execution, and
|
|
// provides no security of its own. It is assumed that any user uses
|
|
// it with an appropriately-configured firewall between their VM
|
|
// instances.
|
|
package main // import "golang.org/x/build/cmd/buildlet"
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha1"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"cloud.google.com/go/compute/metadata"
|
|
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"golang.org/x/build/buildlet"
|
|
"golang.org/x/build/internal/cloud"
|
|
"golang.org/x/build/pargzip"
|
|
)
|
|
|
|
var (
|
|
haltEntireOS = flag.Bool("halt", true, "halt OS in /halt handler. If false, the buildlet process just ends.")
|
|
rebootOnHalt = flag.Bool("reboot", false, "reboot system in /halt handler.")
|
|
workDir = flag.String("workdir", "", "Temporary directory to use. The contents of this directory may be deleted at any time. If empty, TempDir is used to create one.")
|
|
listenAddr = flag.String("listen", "AUTO", "address to listen on. Unused in reverse mode. Warning: this service is inherently insecure and offers no protection of its own. Do not expose this port to the world.")
|
|
reverseType = flag.String("reverse-type", "", "if non-empty, go into reverse mode where the buildlet dials the coordinator instead of listening for connections. The value is the dashboard/builders.go Hosts map key, naming a HostConfig. This buildlet will receive work for any BuildConfig specifying this named HostConfig.")
|
|
coordinator = flag.String("coordinator", "localhost:8119", "address of coordinator, in production use farmer.golang.org. Only used in reverse mode.")
|
|
hostname = flag.String("hostname", "", "hostname to advertise to coordinator for reverse mode; default is actual hostname")
|
|
)
|
|
|
|
// Bump this whenever something notable happens, or when another
|
|
// component needs a certain feature. This shows on the coordinator
|
|
// per reverse client, and is also accessible via the buildlet
|
|
// package's client API (via the Status method).
|
|
//
|
|
// Notable versions:
|
|
// 3: switched to revdial protocol
|
|
// 5: reverse dialing uses timeouts+tcp keepalives, pargzip fix
|
|
// 7: version bumps while debugging revdial hang (Issue 12816)
|
|
// 8: mac screensaver disabled
|
|
// 11: move from self-signed cert to LetsEncrypt (Issue 16442)
|
|
// 15: ssh support
|
|
// 16: make macstadium builders always haltEntireOS
|
|
// 17: make macstadium halts use sudo
|
|
// 18: set TMPDIR and GOCACHE
|
|
// 21: GO_BUILDER_SET_GOPROXY=coordinator support
|
|
// 22: TrimSpace the reverse buildlet's gobuildkey contents
|
|
// 23: revdial v2
|
|
// 24: removeAllIncludingReadonly
|
|
// 25: use removeAllIncludingReadonly for all work area cleanup
|
|
const buildletVersion = 25
|
|
|
|
func defaultListenAddr() string {
|
|
if runtime.GOOS == "darwin" {
|
|
// Darwin will never run on GCE, so let's always
|
|
// listen on a high port (so we don't need to be
|
|
// root).
|
|
return ":5936"
|
|
}
|
|
// check if if env is dev
|
|
if !metadata.OnGCE() && !onEC2() {
|
|
return "localhost:5936"
|
|
}
|
|
// In production, default to port 80 or 443, depending on
|
|
// whether TLS is configured.
|
|
if metadataValue(metaKeyTLSCert) != "" {
|
|
return ":443"
|
|
}
|
|
return ":80"
|
|
}
|
|
|
|
// Functionality set non-nil by some platforms:
|
|
var (
|
|
osHalt func()
|
|
configureSerialLogOutput func()
|
|
setOSRlimit func() error
|
|
)
|
|
|
|
// If non-empty, the $TMPDIR and $GOCACHE environment variables to use
|
|
// for child processes.
|
|
var (
|
|
processTmpDirEnv string
|
|
processGoCacheEnv string
|
|
)
|
|
|
|
const (
|
|
metaKeyPassword = "password"
|
|
metaKeyTLSCert = "tls-cert"
|
|
metaKeyTLSkey = "tls-key"
|
|
)
|
|
|
|
func main() {
|
|
builderEnv := os.Getenv("GO_BUILDER_ENV")
|
|
|
|
switch builderEnv {
|
|
case "macstadium_vm":
|
|
configureMacStadium()
|
|
case "linux-arm-arm5spacemonkey":
|
|
initBaseUnixEnv() // Issue 28041
|
|
}
|
|
|
|
onGCE := metadata.OnGCE()
|
|
switch runtime.GOOS {
|
|
case "plan9":
|
|
log.SetOutput(&plan9LogWriter{w: os.Stderr})
|
|
case "linux":
|
|
if onGCE && !inKube {
|
|
if w, err := os.OpenFile("/dev/console", os.O_WRONLY, 0); err == nil {
|
|
log.SetOutput(w)
|
|
}
|
|
}
|
|
case "windows":
|
|
if onGCE {
|
|
configureSerialLogOutput()
|
|
}
|
|
}
|
|
|
|
log.Printf("buildlet starting.")
|
|
flag.Parse()
|
|
|
|
if builderEnv == "android-amd64-emu" {
|
|
startAndroidEmulator()
|
|
}
|
|
|
|
// Optimize emphemeral filesystems. Prefer speed over safety,
|
|
// since these VMs only last for the duration of one build.
|
|
switch runtime.GOOS {
|
|
case "openbsd", "freebsd", "netbsd":
|
|
makeBSDFilesystemFast()
|
|
}
|
|
if setOSRlimit != nil {
|
|
err := setOSRlimit()
|
|
if err != nil {
|
|
log.Fatalf("setOSRLimit: %v", err)
|
|
}
|
|
log.Printf("set OS rlimits.")
|
|
}
|
|
|
|
isReverse := *reverseType != ""
|
|
|
|
if *listenAddr == "AUTO" && !isReverse {
|
|
v := defaultListenAddr()
|
|
log.Printf("Will listen on %s", v)
|
|
*listenAddr = v
|
|
}
|
|
|
|
if !onGCE && !isReverse && !onEC2() && !strings.HasPrefix(*listenAddr, "localhost:") {
|
|
log.Printf("** WARNING *** This server is unsafe and offers no security. Be careful.")
|
|
}
|
|
if onGCE {
|
|
fixMTU()
|
|
}
|
|
if *workDir == "" && setWorkdirToTmpfs != nil {
|
|
setWorkdirToTmpfs()
|
|
}
|
|
if *workDir == "" {
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
// We want a short path on Windows, due to
|
|
// Windows issues with maximum path lengths.
|
|
*workDir = `C:\workdir`
|
|
if err := os.MkdirAll(*workDir, 0755); err != nil {
|
|
log.Fatalf("error creating workdir: %v", err)
|
|
}
|
|
default:
|
|
wdName := "workdir"
|
|
if *reverseType != "" {
|
|
wdName += "-" + *reverseType
|
|
}
|
|
dir := filepath.Join(os.TempDir(), wdName)
|
|
removeAllAndMkdir(dir)
|
|
*workDir = dir
|
|
}
|
|
}
|
|
|
|
os.Setenv("WORKDIR", *workDir) // mostly for demos
|
|
|
|
if _, err := os.Lstat(*workDir); err != nil {
|
|
log.Fatalf("invalid --workdir %q: %v", *workDir, err)
|
|
}
|
|
|
|
// Set up and clean $TMPDIR and $GOCACHE directories.
|
|
if runtime.GOOS != "windows" && runtime.GOOS != "plan9" {
|
|
processTmpDirEnv = filepath.Join(*workDir, "tmp")
|
|
processGoCacheEnv = filepath.Join(*workDir, "gocache")
|
|
removeAllAndMkdir(processTmpDirEnv)
|
|
removeAllAndMkdir(processGoCacheEnv)
|
|
}
|
|
|
|
initGorootBootstrap()
|
|
|
|
http.HandleFunc("/", handleRoot)
|
|
http.HandleFunc("/debug/goroutines", handleGoroutines)
|
|
http.HandleFunc("/debug/x", handleX)
|
|
|
|
var password string
|
|
if !isReverse {
|
|
password = metadataValue(metaKeyPassword)
|
|
}
|
|
requireAuth := func(handler func(w http.ResponseWriter, r *http.Request)) http.Handler {
|
|
return requirePasswordHandler{http.HandlerFunc(handler), password}
|
|
}
|
|
http.Handle("/writetgz", requireAuth(handleWriteTGZ))
|
|
http.Handle("/write", requireAuth(handleWrite))
|
|
http.Handle("/exec", requireAuth(handleExec))
|
|
http.Handle("/halt", requireAuth(handleHalt))
|
|
http.Handle("/tgz", requireAuth(handleGetTGZ))
|
|
http.Handle("/removeall", requireAuth(handleRemoveAll))
|
|
http.Handle("/workdir", requireAuth(handleWorkDir))
|
|
http.Handle("/status", requireAuth(handleStatus))
|
|
http.Handle("/ls", requireAuth(handleLs))
|
|
http.Handle("/connect-ssh", requireAuth(handleConnectSSH))
|
|
|
|
if !isReverse {
|
|
listenForCoordinator()
|
|
} else {
|
|
if err := dialCoordinator(); err != nil {
|
|
log.Fatalf("Error dialing coordinator: %v", err)
|
|
}
|
|
log.Printf("buildlet reverse mode exiting.")
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
var inheritedGorootBootstrap string
|
|
|
|
func initGorootBootstrap() {
|
|
// Remember any GOROOT_BOOTSTRAP to use as a backup in handleExec
|
|
// if $WORKDIR/go1.4 ends up not existing.
|
|
inheritedGorootBootstrap = os.Getenv("GOROOT_BOOTSTRAP")
|
|
|
|
// Default if not otherwise configured in dashboard/builders.go:
|
|
os.Setenv("GOROOT_BOOTSTRAP", filepath.Join(*workDir, "go1.4"))
|
|
}
|
|
|
|
func listenForCoordinator() {
|
|
tlsCert, tlsKey := metadataValue(metaKeyTLSCert), metadataValue(metaKeyTLSkey)
|
|
if (tlsCert == "") != (tlsKey == "") {
|
|
log.Fatalf("tls-cert and tls-key must both be supplied, or neither.")
|
|
}
|
|
|
|
log.Printf("Listening on %s ...", *listenAddr)
|
|
ln, err := net.Listen("tcp", *listenAddr)
|
|
if err != nil {
|
|
log.Fatalf("Failed to listen on %s: %v", *listenAddr, err)
|
|
}
|
|
ln = tcpKeepAliveListener{ln.(*net.TCPListener)}
|
|
|
|
var srv http.Server
|
|
if tlsCert != "" {
|
|
cert, err := tls.X509KeyPair([]byte(tlsCert), []byte(tlsKey))
|
|
if err != nil {
|
|
log.Fatalf("TLS cert error: %v", err)
|
|
}
|
|
tlsConf := &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
}
|
|
ln = tls.NewListener(ln, tlsConf)
|
|
}
|
|
|
|
serveErr := make(chan error, 1)
|
|
go func() {
|
|
serveErr <- srv.Serve(ln)
|
|
}()
|
|
|
|
signalChan := make(chan os.Signal, 1)
|
|
if registerSignal != nil {
|
|
registerSignal(signalChan)
|
|
}
|
|
select {
|
|
case sig := <-signalChan:
|
|
log.Printf("received signal %v; shutting down gracefully.", sig)
|
|
case err := <-serveErr:
|
|
log.Fatalf("Serve: %v", err)
|
|
}
|
|
time.AfterFunc(5*time.Second, func() {
|
|
log.Printf("timeout shutting down gracefully; exiting immediately")
|
|
os.Exit(1)
|
|
})
|
|
if err := srv.Shutdown(context.Background()); err != nil {
|
|
log.Printf("Graceful shutdown error: %v; exiting immediately instead", err)
|
|
os.Exit(1)
|
|
}
|
|
log.Printf("graceful shutdown complete.")
|
|
os.Exit(0)
|
|
}
|
|
|
|
// registerSignal if non-nil registers shutdown signals with the provided chan.
|
|
var registerSignal func(chan<- os.Signal)
|
|
|
|
var inKube = os.Getenv("KUBERNETES_SERVICE_HOST") != ""
|
|
|
|
var (
|
|
// ec2UD contains a copy of the EC2 vm user data retrieved from the metadata.
|
|
ec2UD *cloud.EC2UserData
|
|
// ec2MdC is an EC2 metadata client.
|
|
ec2MdC *ec2metadata.EC2Metadata
|
|
)
|
|
|
|
// onEC2 evaluates if the buildlet is running on an EC2 instance.
|
|
func onEC2() bool {
|
|
if ec2MdC != nil {
|
|
return ec2MdC.Available()
|
|
}
|
|
ses, err := session.NewSession()
|
|
if err != nil {
|
|
log.Printf("unable to create aws session: %s", err)
|
|
return false
|
|
}
|
|
ec2MdC = ec2metadata.New(ses)
|
|
return ec2MdC.Available()
|
|
}
|
|
|
|
// mdValueFromUserData maps a metadata key value into the corresponding
|
|
// EC2UserData value. If a mapping is not found, an empty string is returned.
|
|
func mdValueFromUserData(ud *cloud.EC2UserData, key string) string {
|
|
switch key {
|
|
case metaKeyTLSCert:
|
|
return ud.TLSCert
|
|
case metaKeyTLSkey:
|
|
return ud.TLSKey
|
|
case metaKeyPassword:
|
|
return ud.TLSPassword
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// metadataValue returns the GCE metadata instance value for the given key.
|
|
// If the instance is on EC2 the corresponding value will be extracted from
|
|
// the user data available via the metadata.
|
|
// If the metadata is not defined, the returned string is empty.
|
|
//
|
|
// If not running on GCE or EC2, it falls back to using environment variables
|
|
// for local development.
|
|
func metadataValue(key string) string {
|
|
// The common case (on GCE, but not in Kubernetes):
|
|
if metadata.OnGCE() && !inKube {
|
|
v, err := metadata.InstanceAttributeValue(key)
|
|
if _, notDefined := err.(metadata.NotDefinedError); notDefined {
|
|
return ""
|
|
}
|
|
if err != nil {
|
|
log.Fatalf("metadata.InstanceAttributeValue(%q): %v", key, err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
if onEC2() {
|
|
if ec2UD != nil {
|
|
return mdValueFromUserData(ec2UD, key)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
ec2MetaJson, err := ec2MdC.GetUserDataWithContext(ctx)
|
|
if err != nil {
|
|
log.Fatalf("unable to retrieve EC2 user data: %v", err)
|
|
}
|
|
ec2UD = &cloud.EC2UserData{}
|
|
err = json.Unmarshal([]byte(ec2MetaJson), ec2UD)
|
|
if err != nil {
|
|
log.Fatalf("unable to unmarshal user data json: %v", err)
|
|
}
|
|
return mdValueFromUserData(ec2UD, key)
|
|
}
|
|
|
|
// Else allow use of environment variables to fake
|
|
// metadata keys, for Kubernetes pods or local testing.
|
|
envKey := "META_" + strings.Replace(key, "-", "_", -1)
|
|
v := os.Getenv(envKey)
|
|
// Respect curl-style '@' prefix to mean the rest is a filename.
|
|
if strings.HasPrefix(v, "@") {
|
|
slurp, err := ioutil.ReadFile(v[1:])
|
|
if err != nil {
|
|
log.Fatalf("Error reading file for GCEMETA_%v: %v", key, err)
|
|
}
|
|
return string(slurp)
|
|
}
|
|
if v == "" {
|
|
log.Printf("Warning: not running on GCE, and no %v environment variable defined", envKey)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// tcpKeepAliveListener is a net.Listener that sets TCP keep-alive
|
|
// timeouts on accepted connections.
|
|
type tcpKeepAliveListener struct {
|
|
*net.TCPListener
|
|
}
|
|
|
|
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
|
tc, err := ln.AcceptTCP()
|
|
if err != nil {
|
|
return
|
|
}
|
|
tc.SetKeepAlive(true)
|
|
tc.SetKeepAlivePeriod(3 * time.Minute)
|
|
return tc, nil
|
|
}
|
|
|
|
func fixMTU_freebsd() error { return fixMTU_ifconfig("vtnet0") }
|
|
func fixMTU_openbsd() error { return fixMTU_ifconfig("vio0") }
|
|
func fixMTU_ifconfig(iface string) error {
|
|
out, err := exec.Command("/sbin/ifconfig", iface, "mtu", "1460").CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("/sbin/ifconfig %s mtu 1460: %v, %s", iface, err, out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fixMTU_plan9() error {
|
|
f, err := os.OpenFile("/net/ipifc/0/ctl", os.O_WRONLY, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(f, "mtu 1460\n"); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
return f.Close()
|
|
}
|
|
|
|
func fixMTU() {
|
|
fn, ok := map[string]func() error{
|
|
"openbsd": fixMTU_openbsd,
|
|
"freebsd": fixMTU_freebsd,
|
|
"plan9": fixMTU_plan9,
|
|
}[runtime.GOOS]
|
|
if ok {
|
|
if err := fn(); err != nil {
|
|
log.Printf("Failed to set MTU: %v", err)
|
|
} else {
|
|
log.Printf("Adjusted MTU.")
|
|
}
|
|
}
|
|
}
|
|
|
|
// flushWriter is an io.Writer that Flushes after each Write if the
|
|
// underlying Writer implements http.Flusher.
|
|
type flushWriter struct {
|
|
rw http.ResponseWriter
|
|
}
|
|
|
|
func (fw flushWriter) Write(p []byte) (n int, err error) {
|
|
n, err = fw.rw.Write(p)
|
|
if f, ok := fw.rw.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
return
|
|
}
|
|
|
|
func handleRoot(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
fmt.Fprintf(w, "buildlet running on %s-%s\n", runtime.GOOS, runtime.GOARCH)
|
|
}
|
|
|
|
// unauthenticated /debug/goroutines handler
|
|
func handleGoroutines(w http.ResponseWriter, r *http.Request) {
|
|
log.Printf("Dumping goroutines.")
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
buf := make([]byte, 2<<20)
|
|
buf = buf[:runtime.Stack(buf, true)]
|
|
w.Write(buf)
|
|
log.Printf("Dumped goroutines.")
|
|
}
|
|
|
|
// unauthenticated /debug/x handler, to test MTU settings.
|
|
func handleX(w http.ResponseWriter, r *http.Request) {
|
|
n, _ := strconv.Atoi(r.FormValue("n"))
|
|
if n > 1<<20 {
|
|
n = 1 << 20
|
|
}
|
|
log.Printf("Dumping %d X.", n)
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
buf := make([]byte, n)
|
|
for i := range buf {
|
|
buf[i] = 'X'
|
|
}
|
|
w.Write(buf)
|
|
log.Printf("Dumped X.")
|
|
}
|
|
|
|
// This is a remote code execution daemon, so security is kinda pointless, but:
|
|
func validRelativeDir(dir string) bool {
|
|
if strings.Contains(dir, `\`) || path.IsAbs(dir) {
|
|
return false
|
|
}
|
|
dir = path.Clean(dir)
|
|
if strings.HasPrefix(dir, "../") || strings.HasSuffix(dir, "/..") || dir == ".." {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func handleGetTGZ(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "requires GET method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if !mkdirAllWorkdirOr500(w) {
|
|
return
|
|
}
|
|
dir := r.FormValue("dir")
|
|
if !validRelativeDir(dir) {
|
|
http.Error(w, "bogus dir", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var zw io.WriteCloser
|
|
if r.FormValue("pargzip") == "0" {
|
|
zw = gzip.NewWriter(w)
|
|
} else {
|
|
zw = pargzip.NewWriter(w)
|
|
}
|
|
tw := tar.NewWriter(zw)
|
|
base := filepath.Join(*workDir, filepath.FromSlash(dir))
|
|
err := filepath.Walk(base, func(path string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel := strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(path, base)), "/")
|
|
var linkName string
|
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|
linkName, err = os.Readlink(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
th, err := tar.FileInfoHeader(fi, linkName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
th.Name = rel
|
|
if fi.IsDir() && !strings.HasSuffix(th.Name, "/") {
|
|
th.Name += "/"
|
|
}
|
|
if th.Name == "/" {
|
|
return nil
|
|
}
|
|
if err := tw.WriteHeader(th); err != nil {
|
|
return err
|
|
}
|
|
if fi.Mode().IsRegular() {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
if _, err := io.Copy(tw, f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Printf("Walk error: %v", err)
|
|
panic(http.ErrAbortHandler)
|
|
}
|
|
tw.Close()
|
|
zw.Close()
|
|
}
|
|
|
|
func handleWriteTGZ(w http.ResponseWriter, r *http.Request) {
|
|
if !mkdirAllWorkdirOr500(w) {
|
|
return
|
|
}
|
|
urlParam, _ := url.ParseQuery(r.URL.RawQuery)
|
|
baseDir := *workDir
|
|
if dir := urlParam.Get("dir"); dir != "" {
|
|
if !validRelativeDir(dir) {
|
|
log.Printf("writetgz: bogus dir %q", dir)
|
|
http.Error(w, "bogus dir", http.StatusBadRequest)
|
|
return
|
|
}
|
|
dir = filepath.FromSlash(dir)
|
|
baseDir = filepath.Join(baseDir, dir)
|
|
|
|
// Special case: if the directory is "go1.4" and it already exists, do nothing.
|
|
// This lets clients do a blind write to it and not do extra work.
|
|
if r.Method == "POST" && dir == "go1.4" {
|
|
if fi, err := os.Stat(baseDir); err == nil && fi.IsDir() {
|
|
log.Printf("writetgz: skipping URL puttar to go1.4 dir; already exists")
|
|
io.WriteString(w, "SKIP")
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
|
log.Printf("writetgz: %v", err)
|
|
http.Error(w, "mkdir of base: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
var tgz io.Reader
|
|
var urlStr string
|
|
switch r.Method {
|
|
case "PUT":
|
|
tgz = r.Body
|
|
log.Printf("writetgz: untarring Request.Body into %s", baseDir)
|
|
case "POST":
|
|
urlStr = r.FormValue("url")
|
|
if urlStr == "" {
|
|
log.Printf("writetgz: missing url POST param")
|
|
http.Error(w, "missing url POST param", http.StatusBadRequest)
|
|
return
|
|
}
|
|
t0 := time.Now()
|
|
res, err := http.Get(urlStr)
|
|
if err != nil {
|
|
log.Printf("writetgz: failed to fetch tgz URL %s: %v", urlStr, err)
|
|
http.Error(w, fmt.Sprintf("fetching URL %s: %v", urlStr, err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
log.Printf("writetgz: failed to fetch tgz URL %s: status=%v", urlStr, res.Status)
|
|
http.Error(w, fmt.Sprintf("writetgz: fetching provided URL %q: %s", urlStr, res.Status), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
tgz = res.Body
|
|
log.Printf("writetgz: untarring %s (got headers in %v) into %s", urlStr, time.Since(t0), baseDir)
|
|
default:
|
|
log.Printf("writetgz: invalid method %q", r.Method)
|
|
http.Error(w, "requires PUT or POST method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
err := untar(tgz, baseDir)
|
|
if err != nil {
|
|
status := http.StatusInternalServerError
|
|
if he, ok := err.(httpStatuser); ok {
|
|
status = he.httpStatus()
|
|
}
|
|
http.Error(w, err.Error(), status)
|
|
return
|
|
}
|
|
io.WriteString(w, "OK")
|
|
}
|
|
|
|
func handleWrite(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "PUT" {
|
|
http.Error(w, "requires POST method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
param, _ := url.ParseQuery(r.URL.RawQuery)
|
|
|
|
path := param.Get("path")
|
|
if path == "" || !validRelPath(path) {
|
|
http.Error(w, "bad path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
path = filepath.FromSlash(path)
|
|
path = filepath.Join(*workDir, path)
|
|
|
|
modeInt, err := strconv.ParseInt(param.Get("mode"), 10, 64)
|
|
mode := os.FileMode(modeInt)
|
|
if err != nil || !mode.IsRegular() {
|
|
http.Error(w, "bad mode", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Make the directory if it doesn't exist.
|
|
// TODO(adg): support dirmode parameter?
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err := writeFile(r.Body, path, mode); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
io.WriteString(w, "OK")
|
|
}
|
|
|
|
func writeFile(r io.Reader, path string, mode os.FileMode) error {
|
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(f, r); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
// Try to set the mode again, in case the file already existed.
|
|
if runtime.GOOS != "windows" {
|
|
if err := f.Chmod(mode); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
}
|
|
return f.Close()
|
|
}
|
|
|
|
// untar reads the gzip-compressed tar file from r and writes it into dir.
|
|
func untar(r io.Reader, dir string) (err error) {
|
|
t0 := time.Now()
|
|
nFiles := 0
|
|
madeDir := map[string]bool{}
|
|
defer func() {
|
|
td := time.Since(t0)
|
|
if err == nil {
|
|
log.Printf("extracted tarball into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td)
|
|
} else {
|
|
log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err)
|
|
}
|
|
}()
|
|
zr, err := gzip.NewReader(r)
|
|
if err != nil {
|
|
return badRequest("requires gzip-compressed body: " + err.Error())
|
|
}
|
|
tr := tar.NewReader(zr)
|
|
loggedChtimesError := false
|
|
for {
|
|
f, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
log.Printf("tar reading error: %v", err)
|
|
return badRequest("tar error: " + err.Error())
|
|
}
|
|
if f.Typeflag == tar.TypeXGlobalHeader {
|
|
// golang.org/issue/22748: git archive exports
|
|
// a global header ('g') which after Go 1.9
|
|
// (for a bit?) contained an empty filename.
|
|
// Ignore it.
|
|
continue
|
|
}
|
|
if !validRelPath(f.Name) {
|
|
return badRequest(fmt.Sprintf("tar file contained invalid name %q", f.Name))
|
|
}
|
|
rel := filepath.FromSlash(f.Name)
|
|
abs := filepath.Join(dir, rel)
|
|
|
|
fi := f.FileInfo()
|
|
mode := fi.Mode()
|
|
switch {
|
|
case mode.IsRegular():
|
|
// Make the directory. This is redundant because it should
|
|
// already be made by a directory entry in the tar
|
|
// beforehand. Thus, don't check for errors; the next
|
|
// write will fail with the same error.
|
|
dir := filepath.Dir(abs)
|
|
if !madeDir[dir] {
|
|
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
|
|
return err
|
|
}
|
|
madeDir[dir] = true
|
|
}
|
|
wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, err := io.Copy(wf, tr)
|
|
if closeErr := wf.Close(); closeErr != nil && err == nil {
|
|
err = closeErr
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("error writing to %s: %v", abs, err)
|
|
}
|
|
if n != f.Size {
|
|
return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
|
|
}
|
|
modTime := f.ModTime
|
|
if modTime.After(t0) {
|
|
// Clamp modtimes at system time. See
|
|
// golang.org/issue/19062 when clock on
|
|
// buildlet was behind the gitmirror server
|
|
// doing the git-archive.
|
|
modTime = t0
|
|
}
|
|
if !modTime.IsZero() {
|
|
if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
|
|
// benign error. Gerrit doesn't even set the
|
|
// modtime in these, and we don't end up relying
|
|
// on it anywhere (the gomote push command relies
|
|
// on digests only), so this is a little pointless
|
|
// for now.
|
|
log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
|
|
loggedChtimesError = true // once is enough
|
|
}
|
|
}
|
|
nFiles++
|
|
case mode.IsDir():
|
|
if err := os.MkdirAll(abs, 0755); err != nil {
|
|
return err
|
|
}
|
|
madeDir[abs] = true
|
|
case mode&os.ModeSymlink != 0:
|
|
// TODO: ignore these for now. They were breaking x/build tests.
|
|
// Implement these if/when we ever have a test that needs them.
|
|
// But maybe we'd have to skip creating them on Windows for some builders
|
|
// without permissions.
|
|
default:
|
|
return badRequest(fmt.Sprintf("tar file entry %s contained unsupported file type %v", f.Name, mode))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Process-State is an HTTP Trailer set in the /exec handler to "ok"
|
|
// on success, or os.ProcessState.String() on failure.
|
|
const hdrProcessState = "Process-State"
|
|
|
|
func handleExec(w http.ResponseWriter, r *http.Request) {
|
|
cn := w.(http.CloseNotifier)
|
|
clientGone := cn.CloseNotify()
|
|
handlerDone := make(chan bool)
|
|
defer close(handlerDone)
|
|
|
|
if r.Method != "POST" {
|
|
http.Error(w, "requires POST method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if r.ProtoMajor*10+r.ProtoMinor < 11 {
|
|
// We need trailers, only available in HTTP/1.1 or HTTP/2.
|
|
http.Error(w, "HTTP/1.1 or higher required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
// Create *workDir and (if needed) tmp and gocache.
|
|
if !mkdirAllWorkdirOr500(w) {
|
|
return
|
|
}
|
|
for _, dir := range []string{processTmpDirEnv, processGoCacheEnv} {
|
|
if dir == "" {
|
|
continue
|
|
}
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
if err := checkAndroidEmulator(); err != nil {
|
|
http.Error(w, "android emulator not running: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Trailer", hdrProcessState) // declare it so we can set it
|
|
|
|
cmdPath := r.FormValue("cmd") // required
|
|
absCmd := cmdPath
|
|
dir := r.FormValue("dir") // optional
|
|
sysMode := r.FormValue("mode") == "sys"
|
|
debug, _ := strconv.ParseBool(r.FormValue("debug"))
|
|
|
|
if sysMode {
|
|
if cmdPath == "" {
|
|
http.Error(w, "requires 'cmd' parameter", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if dir == "" {
|
|
dir = *workDir
|
|
} else {
|
|
dir = filepath.FromSlash(dir)
|
|
if !filepath.IsAbs(dir) {
|
|
dir = filepath.Join(*workDir, dir)
|
|
}
|
|
}
|
|
} else {
|
|
if !validRelPath(cmdPath) {
|
|
http.Error(w, "requires 'cmd' parameter", http.StatusBadRequest)
|
|
return
|
|
}
|
|
absCmd = filepath.Join(*workDir, filepath.FromSlash(cmdPath))
|
|
if dir == "" {
|
|
dir = filepath.Dir(absCmd)
|
|
} else {
|
|
if !validRelPath(dir) {
|
|
http.Error(w, "bogus 'dir' parameter", http.StatusBadRequest)
|
|
return
|
|
}
|
|
dir = filepath.Join(*workDir, filepath.FromSlash(dir))
|
|
}
|
|
}
|
|
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
|
|
postEnv := r.PostForm["env"]
|
|
|
|
goarch := "amd64" // unless we find otherwise
|
|
if v := getEnv(postEnv, "GOARCH"); v != "" {
|
|
goarch = v
|
|
}
|
|
if v, _ := strconv.ParseBool(getEnv(postEnv, "GO_DISABLE_OUTBOUND_NETWORK")); v {
|
|
disableOutboundNetwork()
|
|
}
|
|
|
|
env := append(baseEnv(goarch), postEnv...)
|
|
|
|
if v := processTmpDirEnv; v != "" {
|
|
env = append(env, "TMPDIR="+v)
|
|
}
|
|
if v := processGoCacheEnv; v != "" {
|
|
env = append(env, "GOCACHE="+v)
|
|
}
|
|
|
|
// Prefer buildlet process's inherited GOROOT_BOOTSTRAP if
|
|
// there was one and the one we're about to use doesn't exist.
|
|
if v := getEnv(env, "GOROOT_BOOTSTRAP"); v != "" && inheritedGorootBootstrap != "" && pathNotExist(v) {
|
|
env = append(env, "GOROOT_BOOTSTRAP="+inheritedGorootBootstrap)
|
|
}
|
|
env = setPathEnv(env, r.PostForm["path"], *workDir)
|
|
|
|
var cmd *exec.Cmd
|
|
if needsBashWrapper(absCmd) {
|
|
cmd = exec.Command("bash", absCmd)
|
|
} else {
|
|
cmd = exec.Command(absCmd)
|
|
}
|
|
cmd.Args = append(cmd.Args, r.PostForm["cmdArg"]...)
|
|
cmd.Dir = dir
|
|
cmdOutput := flushWriter{w}
|
|
cmd.Stdout = cmdOutput
|
|
cmd.Stderr = cmdOutput
|
|
cmd.Env = env
|
|
|
|
log.Printf("[%p] Running %s with args %q and env %q in dir %s",
|
|
cmd, cmd.Path, cmd.Args, cmd.Env, cmd.Dir)
|
|
|
|
if debug {
|
|
fmt.Fprintf(cmdOutput, ":: Running %s with args %q and env %q in dir %s\n\n",
|
|
cmd.Path, cmd.Args, cmd.Env, cmd.Dir)
|
|
}
|
|
|
|
t0 := time.Now()
|
|
err := cmd.Start()
|
|
if err == nil {
|
|
go func() {
|
|
select {
|
|
case <-clientGone:
|
|
err := killProcessTree(cmd.Process)
|
|
if err != nil {
|
|
log.Printf("Kill failed: %v", err)
|
|
}
|
|
case <-handlerDone:
|
|
return
|
|
}
|
|
}()
|
|
err = cmd.Wait()
|
|
}
|
|
state := "ok"
|
|
if err != nil {
|
|
if ps := cmd.ProcessState; ps != nil {
|
|
state = ps.String()
|
|
} else {
|
|
state = err.Error()
|
|
}
|
|
}
|
|
w.Header().Set(hdrProcessState, state)
|
|
log.Printf("[%p] Run = %s, after %v", cmd, state, time.Since(t0))
|
|
}
|
|
|
|
// needsBashWrappers reports whether the given command needs to
|
|
// run through bash.
|
|
func needsBashWrapper(cmd string) bool {
|
|
if !strings.HasSuffix(cmd, ".bash") {
|
|
return false
|
|
}
|
|
// The mobile platforms can't execute shell scripts directly.
|
|
ismobile := runtime.GOOS == "android" || runtime.GOOS == "ios"
|
|
return ismobile
|
|
}
|
|
|
|
// pathNotExist reports whether path does not exist.
|
|
func pathNotExist(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return os.IsNotExist(err)
|
|
}
|
|
|
|
func getEnv(env []string, key string) string {
|
|
for _, kv := range env {
|
|
if len(kv) <= len(key) || kv[len(key)] != '=' {
|
|
continue
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
// Case insensitive.
|
|
if strings.EqualFold(kv[:len(key)], key) {
|
|
return kv[len(key)+1:]
|
|
}
|
|
} else {
|
|
// Case sensitive.
|
|
if kv[:len(key)] == key {
|
|
return kv[len(key)+1:]
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// setPathEnv returns a copy of the provided environment with any existing
|
|
// PATH variables replaced by the user-provided path.
|
|
// These substitutions are applied to user-supplied path elements:
|
|
// - the string "$PATH" expands to the original PATH elements
|
|
// - the substring "$WORKDIR" expands to the provided workDir
|
|
// A path of just ["$EMPTY"] removes the PATH variable from the environment.
|
|
func setPathEnv(env, path []string, workDir string) []string {
|
|
if len(path) == 0 {
|
|
return env
|
|
}
|
|
|
|
var (
|
|
pathIdx = -1
|
|
pathOrig = ""
|
|
)
|
|
|
|
for i, s := range env {
|
|
if isPathEnvPair(s) {
|
|
pathIdx = i
|
|
pathOrig = s[len("PaTh="):] // in whatever case
|
|
break
|
|
}
|
|
}
|
|
if len(path) == 1 && path[0] == "$EMPTY" {
|
|
// Remove existing path variable if it exists.
|
|
if pathIdx >= 0 {
|
|
env = append(env[:pathIdx], env[pathIdx+1:]...)
|
|
}
|
|
return env
|
|
}
|
|
|
|
// Apply substitions to a copy of the path argument.
|
|
path = append([]string{}, path...)
|
|
for i, s := range path {
|
|
if s == "$PATH" {
|
|
path[i] = pathOrig // ok if empty
|
|
} else {
|
|
path[i] = strings.Replace(s, "$WORKDIR", workDir, -1)
|
|
}
|
|
}
|
|
|
|
// Put the new PATH in env.
|
|
env = append([]string{}, env...)
|
|
pathEnv := pathEnvVar() + "=" + strings.Join(path, pathSeparator())
|
|
if pathIdx >= 0 {
|
|
env[pathIdx] = pathEnv
|
|
} else {
|
|
env = append(env, pathEnv)
|
|
}
|
|
|
|
return env
|
|
}
|
|
|
|
// isPathEnvPair reports whether the key=value pair s represents
|
|
// the operating system's path variable.
|
|
func isPathEnvPair(s string) bool {
|
|
// On Unix it's PATH.
|
|
// On Plan 9 it's path.
|
|
// On Windows it's pAtH case-insensitive.
|
|
if runtime.GOOS == "windows" {
|
|
return len(s) >= 5 && strings.EqualFold(s[:5], "PATH=")
|
|
}
|
|
if runtime.GOOS == "plan9" {
|
|
return strings.HasPrefix(s, "path=")
|
|
}
|
|
return strings.HasPrefix(s, "PATH=")
|
|
}
|
|
|
|
// On Unix it's PATH.
|
|
// On Plan 9 it's path.
|
|
// On Windows it's pAtH case-insensitive.
|
|
func pathEnvVar() string {
|
|
if runtime.GOOS == "plan9" {
|
|
return "path"
|
|
}
|
|
return "PATH"
|
|
}
|
|
|
|
func pathSeparator() string {
|
|
if runtime.GOOS == "plan9" {
|
|
return "\x00"
|
|
} else {
|
|
return string(filepath.ListSeparator)
|
|
}
|
|
}
|
|
|
|
func baseEnv(goarch string) []string {
|
|
if runtime.GOOS == "windows" {
|
|
return windowsBaseEnv(goarch)
|
|
}
|
|
return os.Environ()
|
|
}
|
|
|
|
func windowsBaseEnv(goarch string) (e []string) {
|
|
e = append(e, "GOBUILDEXIT=1") // exit all.bat with completion status
|
|
|
|
is64 := goarch != "386"
|
|
for _, pair := range os.Environ() {
|
|
const pathEq = "PATH="
|
|
if hasPrefixFold(pair, pathEq) {
|
|
e = append(e, "PATH="+windowsPath(pair[len(pathEq):], is64))
|
|
} else {
|
|
e = append(e, pair)
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
|
|
// hasPrefixFold is a case-insensitive strings.HasPrefix.
|
|
func hasPrefixFold(s, prefix string) bool {
|
|
return len(s) >= len(prefix) && strings.EqualFold(s[:len(prefix)], prefix)
|
|
}
|
|
|
|
// windowsPath cleans the windows %PATH% environment.
|
|
// is64Bit is whether this is a windows-amd64-* builder.
|
|
// The PATH is assumed to be that of the image described in env/windows/README.
|
|
func windowsPath(old string, is64Bit bool) string {
|
|
vv := filepath.SplitList(old)
|
|
newPath := make([]string, 0, len(vv))
|
|
|
|
// for windows-buildlet-v2 images
|
|
for _, v := range vv {
|
|
// The base VM image has both the 32-bit and 64-bit gcc installed.
|
|
// They're both in the environment, so scrub the one
|
|
// we don't want (TDM-GCC-64 or TDM-GCC-32).
|
|
if strings.Contains(v, "TDM-GCC-") {
|
|
gcc64 := strings.Contains(v, "TDM-GCC-64")
|
|
if is64Bit != gcc64 {
|
|
continue
|
|
}
|
|
}
|
|
newPath = append(newPath, v)
|
|
}
|
|
|
|
// for windows-amd64-* images
|
|
if is64Bit {
|
|
newPath = append(newPath, `C:\godep\gcc64\bin`)
|
|
} else {
|
|
newPath = append(newPath, `C:\godep\gcc32\bin`)
|
|
}
|
|
|
|
return strings.Join(newPath, string(filepath.ListSeparator))
|
|
}
|
|
|
|
func handleHalt(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "requires POST method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Do the halt in 1 second, to give the HTTP response time to
|
|
// complete.
|
|
//
|
|
// TODO(bradfitz): maybe prevent any (unlikely) future HTTP
|
|
// requests from doing anything from this point on in the
|
|
// remaining second.
|
|
log.Printf("Halting in 1 second.")
|
|
time.AfterFunc(1*time.Second, doHalt)
|
|
}
|
|
|
|
func doHalt() {
|
|
if *rebootOnHalt {
|
|
if err := exec.Command("reboot").Run(); err != nil {
|
|
log.Printf("Error running reboot: %v", err)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
if !*haltEntireOS {
|
|
log.Printf("Ending buildlet process due to halt.")
|
|
os.Exit(0)
|
|
return
|
|
}
|
|
log.Printf("Halting machine.")
|
|
time.AfterFunc(5*time.Second, func() { os.Exit(0) })
|
|
if osHalt != nil {
|
|
// TODO: Windows: http://msdn.microsoft.com/en-us/library/windows/desktop/aa376868%28v=vs.85%29.aspx
|
|
osHalt()
|
|
os.Exit(0)
|
|
}
|
|
// Backup mechanism, if exec hangs for any reason:
|
|
var err error
|
|
switch runtime.GOOS {
|
|
case "openbsd":
|
|
// Quick, no fs flush, and power down:
|
|
err = exec.Command("halt", "-q", "-n", "-p").Run()
|
|
case "freebsd":
|
|
// Power off (-p), via halt (-o), now.
|
|
err = exec.Command("shutdown", "-p", "-o", "now").Run()
|
|
case "linux":
|
|
// Don't sync (-n), force without shutdown (-f), and power off (-p).
|
|
err = exec.Command("/bin/halt", "-n", "-f", "-p").Run()
|
|
case "plan9":
|
|
err = exec.Command("fshalt").Run()
|
|
case "darwin":
|
|
if os.Getenv("GO_BUILDER_ENV") == "macstadium_vm" {
|
|
// Fast, sloppy, unsafe, because we're never reusing this VM again.
|
|
err = exec.Command("/usr/bin/sudo", "/sbin/halt", "-n", "-q", "-l").Run()
|
|
} else {
|
|
err = errors.New("not respecting -halt flag on macOS in unknown environment")
|
|
}
|
|
default:
|
|
err = errors.New("no system-specific halt command run; will just end buildlet process")
|
|
}
|
|
log.Printf("Shutdown: %v", err)
|
|
log.Printf("Ending buildlet process post-halt")
|
|
os.Exit(0)
|
|
}
|
|
|
|
func handleRemoveAll(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "requires POST method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
paths := r.Form["path"]
|
|
if len(paths) == 0 {
|
|
http.Error(w, "requires 'path' parameter", http.StatusBadRequest)
|
|
return
|
|
}
|
|
for _, p := range paths {
|
|
if !validRelPath(p) {
|
|
http.Error(w, fmt.Sprintf("bad 'path' parameter: %q", p), http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
for _, p := range paths {
|
|
log.Printf("Removing %s", p)
|
|
fullDir := filepath.Join(*workDir, filepath.FromSlash(p))
|
|
err := removeAllIncludingReadonly(fullDir)
|
|
if p == "." && err != nil {
|
|
// If workDir is a mountpoint and/or contains a binary
|
|
// using it, we can get a "Device or resource busy" error.
|
|
// See if it's now empty and ignore the error.
|
|
if f, oerr := os.Open(*workDir); oerr == nil {
|
|
if all, derr := f.Readdirnames(-1); derr == nil && len(all) == 0 {
|
|
log.Printf("Ignoring fail of RemoveAll(.)")
|
|
err = nil
|
|
} else {
|
|
log.Printf("Readdir = %q, %v", all, derr)
|
|
}
|
|
f.Close()
|
|
} else {
|
|
log.Printf("Failed to open workdir: %v", oerr)
|
|
}
|
|
}
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// mkdirAllWorkdirOr500 reports whether *workDir either exists or was created.
|
|
// If it returns false, it also writes an HTTP 500 error to w.
|
|
// This is used by callers to verify *workDir exists, even if it might've been
|
|
// deleted previously.
|
|
func mkdirAllWorkdirOr500(w http.ResponseWriter) bool {
|
|
if err := os.MkdirAll(*workDir, 0755); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func handleWorkDir(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "requires GET method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
fmt.Fprint(w, *workDir)
|
|
}
|
|
|
|
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "requires GET method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
status := buildlet.Status{
|
|
Version: buildletVersion,
|
|
}
|
|
b, err := json.Marshal(status)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Write(b)
|
|
}
|
|
|
|
func handleLs(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "requires GET method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
dir := r.FormValue("dir")
|
|
recursive, _ := strconv.ParseBool(r.FormValue("recursive"))
|
|
digest, _ := strconv.ParseBool(r.FormValue("digest"))
|
|
skip := r.Form["skip"] // '/'-separated relative dirs
|
|
|
|
if !mkdirAllWorkdirOr500(w) {
|
|
return
|
|
}
|
|
if !validRelativeDir(dir) {
|
|
http.Error(w, "bogus dir", http.StatusBadRequest)
|
|
return
|
|
}
|
|
base := filepath.Join(*workDir, filepath.FromSlash(dir))
|
|
anyOutput := false
|
|
err := filepath.Walk(base, func(path string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel := strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(path, base)), "/")
|
|
if rel == "" && fi.IsDir() {
|
|
return nil
|
|
}
|
|
if fi.IsDir() {
|
|
for _, v := range skip {
|
|
if rel == v {
|
|
return filepath.SkipDir
|
|
}
|
|
}
|
|
}
|
|
anyOutput = true
|
|
fmt.Fprintf(w, "%s\t%s", fi.Mode(), rel)
|
|
if fi.Mode().IsRegular() {
|
|
fmt.Fprintf(w, "\t%d\t%s", fi.Size(), fi.ModTime().UTC().Format(time.RFC3339))
|
|
if digest {
|
|
if sha1, err := fileSHA1(path); err != nil {
|
|
return err
|
|
} else {
|
|
io.WriteString(w, "\t"+sha1)
|
|
}
|
|
}
|
|
} else if fi.Mode().IsDir() {
|
|
io.WriteString(w, "/")
|
|
}
|
|
io.WriteString(w, "\n")
|
|
if fi.IsDir() && !recursive {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Printf("Walk error: %v", err)
|
|
if anyOutput {
|
|
// Decent way to signal failure to the caller, since it'll break
|
|
// the chunked response, rather than have a valid EOF.
|
|
conn, _, _ := w.(http.Hijacker).Hijack()
|
|
conn.Close()
|
|
return
|
|
}
|
|
http.Error(w, "Walk error: "+err.Error(), 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
func handleConnectSSH(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "requires POST method", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if r.ContentLength != 0 {
|
|
http.Error(w, "requires zero Content-Length", http.StatusBadRequest)
|
|
return
|
|
}
|
|
sshUser := r.Header.Get("X-Go-Ssh-User")
|
|
authKey := r.Header.Get("X-Go-Authorized-Key")
|
|
if sshUser != "" && authKey != "" {
|
|
if err := appendSSHAuthorizedKey(sshUser, authKey); err != nil {
|
|
http.Error(w, "adding ssh authorized key: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
sshServerOnce.Do(startSSHServer)
|
|
|
|
var sshConn net.Conn
|
|
var err error
|
|
|
|
// In theory we shouldn't need retries here at all, but the
|
|
// startSSHServerLinux's use of sshd -D is kinda sketchy and
|
|
// restarts the process whenever we connect to it, so in case
|
|
// it's just down between restarts, try a few times. 5 tries
|
|
// and 5 seconds seems plenty.
|
|
const maxTries = 5
|
|
for try := 1; try <= maxTries; try++ {
|
|
sshConn, err = net.Dial("tcp", "localhost:"+sshPort())
|
|
if err == nil {
|
|
break
|
|
}
|
|
if try == maxTries {
|
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
|
return
|
|
}
|
|
time.Sleep(time.Second)
|
|
}
|
|
defer sshConn.Close()
|
|
hj, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
log.Printf("conn can't hijack for ssh proxy; HTTP/2 enabled by default?")
|
|
http.Error(w, "conn can't hijack", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
conn, _, err := hj.Hijack()
|
|
if err != nil {
|
|
log.Printf("ssh hijack error: %v", err)
|
|
http.Error(w, "ssh hijack error: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: ssh\r\nConnection: Upgrade\r\n\r\n")
|
|
errc := make(chan error, 1)
|
|
go func() {
|
|
_, err := io.Copy(sshConn, conn)
|
|
errc <- err
|
|
}()
|
|
go func() {
|
|
_, err := io.Copy(conn, sshConn)
|
|
errc <- err
|
|
}()
|
|
<-errc
|
|
}
|
|
|
|
// sshPort returns the port to use for the local SSH server.
|
|
func sshPort() string {
|
|
// runningInCOS is whether we're running under GCE's Container-Optimized OS (COS).
|
|
const runningInCOS = runtime.GOOS == "linux" && runtime.GOARCH == "amd64"
|
|
|
|
if runningInCOS {
|
|
// If running in COS, we can't use port 22, as the system's sshd is already using it.
|
|
// Our container runs in the system network namespace, not isolated as is typical
|
|
// in Docker or Kubernetes. So use another high port. See https://golang.org/issue/26969.
|
|
return "2200"
|
|
}
|
|
return "22"
|
|
}
|
|
|
|
var sshServerOnce sync.Once
|
|
|
|
// startSSHServer starts an SSH server.
|
|
func startSSHServer() {
|
|
if inLinuxContainer() {
|
|
startSSHServerLinux()
|
|
return
|
|
}
|
|
if runtime.GOOS == "netbsd" {
|
|
startSSHServerNetBSD()
|
|
return
|
|
}
|
|
|
|
log.Printf("start ssh server: don't know how to start SSH server on this host type")
|
|
}
|
|
|
|
// inLinuxContainer reports whether it looks like we're on Linux running inside a container.
|
|
func inLinuxContainer() bool {
|
|
if runtime.GOOS != "linux" {
|
|
return false
|
|
}
|
|
if numProcs() >= 4 {
|
|
// There should 1 process running (this buildlet
|
|
// binary) if we're in Docker. Maybe 2 if something
|
|
// else is happening. But if there are 4 or more,
|
|
// we'll be paranoid and assuming we're running on a
|
|
// user or host system and don't want to start an ssh
|
|
// server.
|
|
return false
|
|
}
|
|
// TODO: use a more explicit env variable or on-disk signal
|
|
// that we're in a Go buildlet Docker image. But for now, this
|
|
// seems to be consistently true:
|
|
fi, err := os.Stat("/usr/local/bin/stage0")
|
|
return err == nil && fi.Mode().IsRegular()
|
|
}
|
|
|
|
// startSSHServerLinux starts an SSH server on a Linux system.
|
|
func startSSHServerLinux() {
|
|
log.Printf("start ssh server for linux")
|
|
|
|
// First, create the privsep directory, otherwise we get a successful cmd.Start,
|
|
// but this error message and then an exit:
|
|
// Missing privilege separation directory: /var/run/sshd
|
|
if err := os.MkdirAll("/var/run/sshd", 0700); err != nil {
|
|
log.Printf("creating /var/run/sshd: %v", err)
|
|
return
|
|
}
|
|
|
|
// The scaleway Docker images don't have ssh host keys in
|
|
// their image, at least as of 2017-07-23. So make them first.
|
|
// These are the types sshd -D complains about currently.
|
|
if runtime.GOARCH == "arm" {
|
|
for _, keyType := range []string{"rsa", "dsa", "ed25519", "ecdsa"} {
|
|
file := "/etc/ssh/ssh_host_" + keyType + "_key"
|
|
if _, err := os.Stat(file); err == nil {
|
|
continue
|
|
}
|
|
out, err := exec.Command("/usr/bin/ssh-keygen", "-f", file, "-N", "", "-t", keyType).CombinedOutput()
|
|
log.Printf("ssh-keygen of type %s: err=%v, %s\n", keyType, err, out)
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
// TODO: using sshd -D isn't great as it only
|
|
// handles a single connection and exits.
|
|
// Maybe run in sshd -i (inetd) mode instead,
|
|
// and hook that up to the buildlet directly?
|
|
t0 := time.Now()
|
|
cmd := exec.Command("/usr/sbin/sshd", "-D", "-p", sshPort(), "-d", "-d")
|
|
cmd.Stderr = os.Stderr
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
log.Printf("starting sshd: %v", err)
|
|
return
|
|
}
|
|
log.Printf("sshd started.")
|
|
log.Printf("sshd exited: %v; restarting", cmd.Wait())
|
|
if d := time.Since(t0); d < time.Second {
|
|
time.Sleep(time.Second - d)
|
|
}
|
|
}
|
|
}()
|
|
waitLocalSSH()
|
|
}
|
|
|
|
func startSSHServerNetBSD() {
|
|
cmd := exec.Command("/etc/rc.d/sshd", "start")
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
log.Printf("starting sshd: %v", err)
|
|
return
|
|
}
|
|
log.Printf("sshd started.")
|
|
waitLocalSSH()
|
|
}
|
|
|
|
// waitLocalSSH waits for sshd to start accepting connections.
|
|
func waitLocalSSH() {
|
|
for i := 0; i < 40; i++ {
|
|
time.Sleep(10 * time.Millisecond * time.Duration(i+1))
|
|
c, err := net.Dial("tcp", "localhost:"+sshPort())
|
|
if err == nil {
|
|
c.Close()
|
|
log.Printf("sshd connected.")
|
|
return
|
|
}
|
|
}
|
|
log.Printf("timeout waiting for sshd to come up")
|
|
}
|
|
|
|
func numProcs() int {
|
|
n := 0
|
|
fis, _ := ioutil.ReadDir("/proc")
|
|
for _, fi := range fis {
|
|
if _, err := strconv.Atoi(fi.Name()); err == nil {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func fileSHA1(path string) (string, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer f.Close()
|
|
s1 := sha1.New()
|
|
if _, err := io.Copy(s1, f); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%x", s1.Sum(nil)), nil
|
|
}
|
|
|
|
func validRelPath(p string) bool {
|
|
if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
type httpStatuser interface {
|
|
error
|
|
httpStatus() int
|
|
}
|
|
|
|
type httpError struct {
|
|
statusCode int
|
|
msg string
|
|
}
|
|
|
|
func (he httpError) Error() string { return he.msg }
|
|
func (he httpError) httpStatus() int { return he.statusCode }
|
|
|
|
func badRequest(msg string) error {
|
|
return httpError{http.StatusBadRequest, msg}
|
|
}
|
|
|
|
// requirePassword is an http.Handler auth wrapper that enforces a
|
|
// HTTP Basic password. The username is ignored.
|
|
type requirePasswordHandler struct {
|
|
h http.Handler
|
|
password string // empty means no password
|
|
}
|
|
|
|
func (h requirePasswordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
_, gotPass, _ := r.BasicAuth()
|
|
if h.password != "" && h.password != gotPass {
|
|
http.Error(w, "invalid password", http.StatusForbidden)
|
|
return
|
|
}
|
|
h.h.ServeHTTP(w, r)
|
|
}
|
|
|
|
// plan9LogWriter truncates log writes to 128 bytes,
|
|
// to work around some Plan 9 and/or GCE serial port bug.
|
|
type plan9LogWriter struct {
|
|
w io.Writer
|
|
buf []byte
|
|
}
|
|
|
|
func (pw *plan9LogWriter) Write(p []byte) (n int, err error) {
|
|
const max = 128 - len("\n\x00")
|
|
if len(p) < max {
|
|
return pw.w.Write(p)
|
|
}
|
|
if pw.buf == nil {
|
|
pw.buf = make([]byte, max+1)
|
|
}
|
|
n = copy(pw.buf[:max], p)
|
|
pw.buf[n] = '\n'
|
|
return pw.w.Write(pw.buf[:n+1])
|
|
}
|
|
|
|
var killProcessTree = killProcessTreeUnix
|
|
|
|
func killProcessTreeUnix(p *os.Process) error {
|
|
return p.Kill()
|
|
}
|
|
|
|
// configureMacStadium configures the buildlet flags for use on a Mac
|
|
// VM running on MacStadium under VMWare.
|
|
func configureMacStadium() {
|
|
*haltEntireOS = true
|
|
|
|
// TODO: setup RAM disk for tmp and set *workDir
|
|
|
|
disableMacScreensaver()
|
|
enableMacDeveloperMode()
|
|
|
|
version, err := exec.Command("sw_vers", "-productVersion").Output()
|
|
if err != nil {
|
|
log.Fatalf("failed to find sw_vers -productVersion: %v", err)
|
|
}
|
|
majorMinor := regexp.MustCompile(`^(\d+)\.(\d+)`)
|
|
m := majorMinor.FindStringSubmatch(string(version))
|
|
if m == nil {
|
|
log.Fatalf("unsupported sw_vers version %q", version)
|
|
}
|
|
major, minor := m[1], m[2] // "10", "12"
|
|
*reverseType = fmt.Sprintf("host-darwin-%s_%s", major, minor)
|
|
*coordinator = "farmer.golang.org:443"
|
|
|
|
// guestName is set by cmd/makemac to something like
|
|
// "mac_10_10_host01b" or "mac_10_12_host01a", which encodes
|
|
// three things: the mac OS version of the guest VM, which
|
|
// physical machine it's on (1 to 10, currently) and which of
|
|
// two possible VMs on that host is running (a or b). For
|
|
// monitoring purposes, we want stable hostnames and don't
|
|
// care which OS version is currently running (which changes
|
|
// constantly), so normalize these to only have the host
|
|
// number and side (a or b), without the OS version. The
|
|
// buildlet will report the OS version to the coordinator
|
|
// anyway. We could in theory do this normalization in the
|
|
// coordinator, but we don't want to put buildlet-specific
|
|
// knowledge there, and this file already contains a bunch of
|
|
// buildlet host-specific configuration, so normalize it here.
|
|
guestName := vmwareGetInfo("guestinfo.name") // "mac_10_12_host01a"
|
|
hostPos := strings.Index(guestName, "_host")
|
|
if hostPos == -1 {
|
|
// Assume cmd/makemac changed its conventions.
|
|
// Maybe all this normalization belongs there anyway,
|
|
// but normalizing here is a safer first step.
|
|
*hostname = guestName
|
|
} else {
|
|
*hostname = "macstadium" + guestName[hostPos:] // "macstadium_host01a"
|
|
}
|
|
}
|
|
|
|
func disableMacScreensaver() {
|
|
err := exec.Command("defaults", "-currentHost", "write", "com.apple.screensaver", "idleTime", "0").Run()
|
|
if err != nil {
|
|
log.Printf("disabling screensaver: %v", err)
|
|
}
|
|
}
|
|
|
|
// enableMacDeveloperMode enables developer mode on macOS for the
|
|
// runtime tests. (Issue 31123)
|
|
//
|
|
// It is best effort; errors are logged but otherwise ignored.
|
|
func enableMacDeveloperMode() {
|
|
// Macs are configured with password-less sudo. Without sudo we get prompts
|
|
// that "SampleTools wants to make changes" that block the buildlet from starting.
|
|
// But oddly, not via gomote. Only during startup. The environment must be different
|
|
// enough that in one case macOS asks for permission (because it can use the GUI?)
|
|
// and in the gomote case (where the environment is largley scrubbed) it can't do
|
|
// the GUI dialog somehow and must just try to do it anyway and finds that passwordless
|
|
// sudo works. But using sudo seems to make it always work.
|
|
// For extra paranoia, use a context to not block start-up.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
out, err := exec.CommandContext(ctx, "/usr/bin/sudo", "/usr/sbin/DevToolsSecurity", "-enable").CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("Error enabling developer mode: %v, %s", err, out)
|
|
return
|
|
}
|
|
log.Printf("DevToolsSecurity: %s", out)
|
|
}
|
|
|
|
func vmwareGetInfo(key string) string {
|
|
cmd := exec.Command("/Library/Application Support/VMware Tools/vmware-tools-daemon",
|
|
"--cmd",
|
|
"info-get "+key)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if strings.Contains(stderr.String(), "No value found") {
|
|
return ""
|
|
}
|
|
log.Fatalf("Error running vmware-tools-daemon --cmd 'info-get %s': %v, %s\n%s", key, err, stderr.Bytes(), stdout.Bytes())
|
|
}
|
|
return strings.TrimSpace(stdout.String())
|
|
}
|
|
|
|
func makeBSDFilesystemFast() {
|
|
if !metadata.OnGCE() {
|
|
log.Printf("Not on GCE; not remounting root filesystem.")
|
|
return
|
|
}
|
|
btype, err := metadata.InstanceAttributeValue("buildlet-host-type")
|
|
if _, ok := err.(metadata.NotDefinedError); ok && len(btype) == 0 {
|
|
log.Printf("Not remounting root filesystem due to missing buildlet-host-type metadata.")
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Printf("Not remounting root filesystem due to failure getting builder type instance metadata: %v", err)
|
|
return
|
|
}
|
|
// Tested on OpenBSD, FreeBSD, and NetBSD:
|
|
out, err := exec.Command("/sbin/mount", "-u", "-o", "async,noatime", "/").CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("Warning: failed to remount %s root filesystem with async,noatime: %v, %s", runtime.GOOS, err, out)
|
|
return
|
|
}
|
|
log.Printf("Remounted / with async,noatime.")
|
|
}
|
|
|
|
func appendSSHAuthorizedKey(sshUser, authKey string) error {
|
|
var homeRoot string
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
homeRoot = "/Users"
|
|
case "plan9":
|
|
return fmt.Errorf("ssh not supported on %v", runtime.GOOS)
|
|
case "windows":
|
|
homeRoot = `C:\Users`
|
|
default:
|
|
homeRoot = "/home"
|
|
if runtime.GOOS == "freebsd" {
|
|
if fi, err := os.Stat("/usr/home/" + sshUser); err == nil && fi.IsDir() {
|
|
homeRoot = "/usr/home"
|
|
}
|
|
}
|
|
if sshUser == "root" {
|
|
homeRoot = "/"
|
|
}
|
|
}
|
|
sshDir := filepath.Join(homeRoot, sshUser, ".ssh")
|
|
if err := os.MkdirAll(sshDir, 0700); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chmod(sshDir, 0700); err != nil {
|
|
return err
|
|
}
|
|
authFile := filepath.Join(sshDir, "authorized_keys")
|
|
exist, err := ioutil.ReadFile(authFile)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
if strings.Contains(string(exist), authKey) {
|
|
return nil
|
|
}
|
|
f, err := os.OpenFile(authFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(f, "%s\n", authKey); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return err
|
|
}
|
|
if runtime.GOOS == "freebsd" {
|
|
exec.Command("/usr/sbin/chown", "-R", sshUser, sshDir).Run()
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
if res, err := exec.Command("icacls.exe", authFile, "/grant", `NT SERVICE\sshd:(R)`).CombinedOutput(); err != nil {
|
|
return fmt.Errorf("setting permissions on authorized_keys with: %v\n%s", err, res)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setWorkdirToTmpfs sets the *workDir (--workdir) flag to /workdir
|
|
// if the flag is empty and /workdir is a tmpfs mount, as it is on the various
|
|
// hosts that use rundockerbuildlet.
|
|
//
|
|
// It is set non-nil on operating systems where the functionality is
|
|
// needed & available. Currently we only use it on Linux.
|
|
var setWorkdirToTmpfs func()
|
|
|
|
func initBaseUnixEnv() {
|
|
if os.Getenv("USER") == "" {
|
|
os.Setenv("USER", "root")
|
|
}
|
|
if os.Getenv("HOME") == "" {
|
|
os.Setenv("HOME", "/root")
|
|
}
|
|
}
|
|
|
|
// removeAllAndMkdir calls removeAllIncludingReadonly and then os.Mkdir on the given
|
|
// dir, failing the process if either step fails.
|
|
func removeAllAndMkdir(dir string) {
|
|
if err := removeAllIncludingReadonly(dir); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(dir, 0755); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// removeAllIncludingReadonly is like os.RemoveAll except that it'll
|
|
// also try to change permissions to work around permission errors
|
|
// when deleting.
|
|
func removeAllIncludingReadonly(dir string) error {
|
|
err := os.RemoveAll(dir)
|
|
if err == nil || !os.IsPermission(err) ||
|
|
runtime.GOOS == "windows" { // different filesystem permission model; also our windows builders are ephemeral single-use VMs anyway
|
|
return err
|
|
}
|
|
// Make a best effort (ignoring errors) attempt to make all
|
|
// files and directories writable before we try to delete them
|
|
// all again.
|
|
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
|
|
const ownerWritable = 0200
|
|
if err != nil || fi.Mode().Perm()&ownerWritable != 0 {
|
|
return nil
|
|
}
|
|
os.Chmod(path, fi.Mode().Perm()|ownerWritable)
|
|
return nil
|
|
})
|
|
return os.RemoveAll(dir)
|
|
}
|
|
|
|
var (
|
|
androidEmuDead = make(chan error) // closed on death
|
|
androidEmuErr error // set prior to channel close
|
|
)
|
|
|
|
func startAndroidEmulator() {
|
|
cmd := exec.Command("/android/sdk/emulator/emulator",
|
|
"@android-avd",
|
|
"-no-audio",
|
|
"-no-window",
|
|
"-no-boot-anim",
|
|
"-no-snapshot-save",
|
|
"-wipe-data", // required to prevent a hang with -no-window when recovering from a snapshot?
|
|
)
|
|
log.Printf("running Android emulator: %v", cmd.Args)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err != nil {
|
|
log.Fatalf("failed to start Android emulator: %v", err)
|
|
}
|
|
go func() {
|
|
err := cmd.Wait()
|
|
if err == nil {
|
|
err = errors.New("exited without error")
|
|
}
|
|
androidEmuErr = err
|
|
close(androidEmuDead)
|
|
}()
|
|
}
|
|
|
|
// checkAndroidEmulator returns an error if this machine is an Android builder
|
|
// and the Android emulator process has exited.
|
|
func checkAndroidEmulator() error {
|
|
select {
|
|
case <-androidEmuDead:
|
|
return androidEmuErr
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var disableNetOnce sync.Once
|
|
|
|
func disableOutboundNetwork() {
|
|
if runtime.GOOS != "linux" {
|
|
return
|
|
}
|
|
disableNetOnce.Do(disableOutboundNetworkLinux)
|
|
}
|
|
|
|
func disableOutboundNetworkLinux() {
|
|
const iptables = "/sbin/iptables"
|
|
const vcsTestGolangOrgIP = "35.184.38.56" // vcs-test.golang.org
|
|
runOrLog(exec.Command(iptables, "-I", "OUTPUT", "1", "-m", "state", "--state", "NEW", "-d", vcsTestGolangOrgIP, "-p", "tcp", "-j", "ACCEPT"))
|
|
runOrLog(exec.Command(iptables, "-I", "OUTPUT", "2", "-m", "state", "--state", "NEW", "-d", "10.0.0.0/8", "-p", "tcp", "-j", "ACCEPT"))
|
|
runOrLog(exec.Command(iptables, "-I", "OUTPUT", "3", "-m", "state", "--state", "NEW", "-p", "tcp", "--dport", "443", "-j", "REJECT", "--reject-with", "icmp-host-prohibited"))
|
|
runOrLog(exec.Command(iptables, "-I", "OUTPUT", "3", "-m", "state", "--state", "NEW", "-p", "tcp", "--dport", "22", "-j", "REJECT", "--reject-with", "icmp-host-prohibited"))
|
|
}
|
|
|
|
func runOrLog(cmd *exec.Cmd) {
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("failed to run %s: %v, %s", cmd.Args, err, out)
|
|
}
|
|
}
|