blob: b3f60cbca3f65e61867b90f93170b581a5cc4a24 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package util
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
)
// RunCommand executes a command on the host and returns the stdout and stderr
// as byte strings.
func RunCommand(ctx context.Context, name string, arg ...string) ([]byte, []byte, error) {
logger.Infof(ctx, "running: %s %q", name, arg)
c := exec.CommandContext(ctx, name, arg...)
var o bytes.Buffer
var e bytes.Buffer
c.Stdout = &o
c.Stderr = &e
err := c.Run()
stdout := o.Bytes()
stderr := e.Bytes()
return stdout, stderr, err
}
// Untar untars a tar.gz file into a directory.
func Untar(ctx context.Context, dst string, src string) error {
logger.Infof(ctx, "untarring %s into %s", src, dst)
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gz.Close()
tr := tar.NewReader(gz)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
header, err := tr.Next()
if err == io.EOF {
return nil
} else if err != nil {
return err
}
if err := untarNext(dst, tr, header); err != nil {
return err
}
}
}
}
func untarNext(dst string, tr *tar.Reader, header *tar.Header) error {
path := filepath.Join(dst, header.Name)
info := header.FileInfo()
if info.IsDir() {
if err := os.MkdirAll(path, info.Mode()); err != nil {
return err
}
} else {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
// Skip entry if path already exists.
if _, err := os.Stat(path); err == nil {
return nil
}
err := AtomicallyWriteFile(path, info.Mode(), func(f *os.File) error {
_, err := io.Copy(f, tr)
return err
})
if err != nil {
return err
}
}
return nil
}
type PackageJSON struct {
Version json.Number `json:"version"`
Content []string `json:"content"`
}
// ParsePackagesJSON parses an update package's packages.json file for the
// express purpose of returning a map of package names and variant keys
// to the package's Merkle root as a value. This mimics the behavior of the
// function that parsed the legacy "packages" file format.
func ParsePackagesJSON(rd io.Reader) (map[string]string, error) {
var p PackageJSON
packages := make(map[string]string)
if err := json.NewDecoder(rd).Decode(&p); err != nil {
return nil, err
}
if p.Version == "" {
return nil, errors.New("version is required in packages.json format")
}
if p.Version != "1" {
return nil, fmt.Errorf("packages.json version 1 is supported; found version %s", p.Version)
}
for _, pkgURL := range p.Content {
u, err := url.Parse(pkgURL)
if err != nil {
return nil, err
}
if u.Scheme != "fuchsia-pkg" {
return nil, fmt.Errorf("%s is not a fuchsia-pkg URL", pkgURL)
}
// Path is optional and if it exists, the variant is also optional.
if u.Path != "" {
pathComponents := strings.Split(u.Path, "/")
if len(pathComponents) >= 1 {
if hash, ok := u.Query()["hash"]; ok {
packages[u.Path[1:]] = hash[0]
} else {
packages[u.Path[1:]] = ""
}
}
}
}
return packages, nil
}
func AtomicallyWriteFile(path string, mode os.FileMode, writeFileFunc func(*os.File) error) error {
dir := filepath.Dir(path)
basename := filepath.Base(path)
tmpfile, err := ioutil.TempFile(dir, basename)
if err != nil {
return err
}
defer func() {
if tmpfile != nil {
tmpfile.Close()
os.Remove(tmpfile.Name())
}
}()
if err = writeFileFunc(tmpfile); err != nil {
return err
}
if err = os.Chmod(tmpfile.Name(), mode); err != nil {
return err
}
// Now that we've written the file, do an atomic swap of the filename into place.
if err := os.Rename(tmpfile.Name(), path); err != nil {
return err
}
if err := tmpfile.Close(); err != nil {
return err
}
tmpfile = nil
return nil
}
// RunWithTimeout runs a closure to completion, or returns an error if it times
// out.
func RunWithTimeout(ctx context.Context, timeout time.Duration, f func() error) error {
return RunWithDeadline(ctx, time.Now().Add(timeout), f)
}
// RunWithDeadline runs a closure to runs the closure in a goroutine
func RunWithDeadline(ctx context.Context, deadline time.Time, f func() error) error {
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
ch := make(chan error, 1)
go func() {
ch <- f()
}()
select {
case err := <-ch:
return err
case <-ctx.Done():
return fmt.Errorf("Function timed out: %w", ctx.Err())
}
}