// Copyright 2017 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package main

import (
	"crypto/sha256"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/google/syzkaller/dashboard/dashapi"
	"github.com/google/syzkaller/pkg/asset"
	"github.com/google/syzkaller/pkg/build"
	"github.com/google/syzkaller/pkg/config"
	"github.com/google/syzkaller/pkg/gcs"
	"github.com/google/syzkaller/pkg/hash"
	"github.com/google/syzkaller/pkg/instance"
	"github.com/google/syzkaller/pkg/log"
	"github.com/google/syzkaller/pkg/mgrconfig"
	"github.com/google/syzkaller/pkg/osutil"
	"github.com/google/syzkaller/pkg/report"
	"github.com/google/syzkaller/pkg/vcs"
	"github.com/google/syzkaller/prog"
	_ "github.com/google/syzkaller/sys"
	"github.com/google/syzkaller/sys/targets"
	"github.com/google/syzkaller/vm"
)

// This is especially slightly longer than syzkaller rebuild period.
// If we set kernelRebuildPeriod = syzkallerRebuildPeriod and both are changed
// during that period (or around that period), we can rebuild kernel, restart
// manager and then instantly shutdown everything for syzkaller update.
// Instead we rebuild syzkaller, restart and then rebuild kernel.
const kernelRebuildPeriod = syzkallerRebuildPeriod + time.Hour

// List of required files in kernel build (contents of latest/current dirs).
var imageFiles = map[string]bool{
	"tag":           true,  // serialized BuildInfo
	"kernel.config": false, // kernel config used for build
	"image":         true,  // kernel image
	"kernel":        false,
	"initrd":        false,
	"key":           false, // root ssh key for the image
}

func init() {
	for _, arches := range targets.List {
		for _, arch := range arches {
			if arch.KernelObject != "" {
				imageFiles["obj/"+arch.KernelObject] = false
			}
		}
	}
}

// Manager represents a single syz-manager instance.
// Handles kernel polling, image rebuild and manager process management.
// As syzkaller builder, it maintains 2 builds:
//   - latest: latest known good kernel build
//   - current: currently used kernel build
type Manager struct {
	name         string
	workDir      string
	kernelDir    string
	currentDir   string
	latestDir    string
	configTag    string
	configData   []byte
	cfg          *Config
	repo         vcs.Repo
	mgrcfg       *ManagerConfig
	managercfg   *mgrconfig.Config
	cmd          *ManagerCmd
	dash         *dashapi.Dashboard
	debugStorage bool
	storage      *asset.Storage
	stop         chan struct{}
	debug        bool
	lastBuild    *dashapi.Build
}

func createManager(cfg *Config, mgrcfg *ManagerConfig, stop chan struct{},
	debug bool) (*Manager, error) {
	dir := osutil.Abs(filepath.Join("managers", mgrcfg.Name))
	err := osutil.MkdirAll(dir)
	if err != nil {
		log.Fatal(err)
	}
	if mgrcfg.RepoAlias == "" {
		mgrcfg.RepoAlias = mgrcfg.Repo
	}

	var dash *dashapi.Dashboard
	if cfg.DashboardAddr != "" && mgrcfg.DashboardClient != "" {
		dash, err = dashapi.New(mgrcfg.DashboardClient, cfg.DashboardAddr, mgrcfg.DashboardKey)
		if err != nil {
			return nil, err
		}
	}
	var assetStorage *asset.Storage
	if !cfg.AssetStorage.IsEmpty() {
		assetStorage, err = asset.StorageFromConfig(cfg.AssetStorage, dash)
		if err != nil {
			log.Fatalf("failed to create asset storage: %v", err)
		}
	}
	var configData []byte
	if mgrcfg.KernelConfig != "" {
		if configData, err = os.ReadFile(mgrcfg.KernelConfig); err != nil {
			return nil, err
		}
	}
	kernelDir := filepath.Join(dir, "kernel")
	repo, err := vcs.NewRepo(mgrcfg.managercfg.TargetOS, mgrcfg.managercfg.Type, kernelDir)
	if err != nil {
		log.Fatalf("failed to create repo for %v: %v", mgrcfg.Name, err)
	}

	mgr := &Manager{
		name:         mgrcfg.managercfg.Name,
		workDir:      filepath.Join(dir, "workdir"),
		kernelDir:    kernelDir,
		currentDir:   filepath.Join(dir, "current"),
		latestDir:    filepath.Join(dir, "latest"),
		configTag:    hash.String(configData),
		configData:   configData,
		cfg:          cfg,
		repo:         repo,
		mgrcfg:       mgrcfg,
		managercfg:   mgrcfg.managercfg,
		dash:         dash,
		storage:      assetStorage,
		debugStorage: !cfg.AssetStorage.IsEmpty() && cfg.AssetStorage.Debug,
		stop:         stop,
		debug:        debug,
	}

	os.RemoveAll(mgr.currentDir)
	return mgr, nil
}

// Gates kernel builds, syzkaller builds and coverage report generation.
// Kernel builds take whole machine, so we don't run more than one at a time.
// Also current image build script uses some global resources (/dev/nbd0) and can't run in parallel.
var buildSem = instance.NewSemaphore(1)

// Gates tests that require extra VMs.
// Currently we overcommit instances in such cases, so we'd like to minimize the number of
// simultaneous env.Test calls.
var testSem = instance.NewSemaphore(1)

func (mgr *Manager) loop() {
	lastCommit := ""
	nextBuildTime := time.Now()
	var managerRestartTime, artifactUploadTime time.Time
	latestInfo := mgr.checkLatest()
	if latestInfo != nil && time.Since(latestInfo.Time) < kernelRebuildPeriod/2 &&
		mgr.managercfg.TargetOS != targets.Fuchsia {
		// If we have a reasonably fresh build,
		// start manager straight away and don't rebuild kernel for a while.
		// Fuchsia is a special case: it builds with syz-executor, so if we just updated syzkaller, we need
		// to rebuild fuchsia as well.
		log.Logf(0, "%v: using latest image built on %v", mgr.name, latestInfo.KernelCommit)
		managerRestartTime = latestInfo.Time
		nextBuildTime = time.Now().Add(kernelRebuildPeriod)
		mgr.restartManager()
	} else if latestInfo != nil {
		log.Logf(0, "%v: latest image is on %v", mgr.name, latestInfo.KernelCommit)
	}

	ticker := time.NewTicker(buildRetryPeriod)
	defer ticker.Stop()

loop:
	for {
		if time.Since(nextBuildTime) >= 0 {
			var rebuildAfter time.Duration
			lastCommit, latestInfo, rebuildAfter = mgr.pollAndBuild(lastCommit, latestInfo)
			nextBuildTime = time.Now().Add(rebuildAfter)
		}
		if !artifactUploadTime.IsZero() && time.Now().After(artifactUploadTime) {
			artifactUploadTime = time.Time{}
			if err := mgr.uploadCoverReport(); err != nil {
				mgr.Errorf("failed to upload cover report: %v", err)
			}
			if mgr.cfg.CorpusUploadPath != "" {
				if err := mgr.uploadCorpus(); err != nil {
					mgr.Errorf("failed to upload corpus: %v", err)
				}
			}
		}

		select {
		case <-mgr.stop:
			break loop
		default:
		}

		if latestInfo != nil && (latestInfo.Time != managerRestartTime || mgr.cmd == nil) {
			managerRestartTime = latestInfo.Time
			mgr.restartManager()
			if mgr.cmd != nil {
				artifactUploadTime = time.Now().Add(6 * time.Hour)
			}
		}

		select {
		case <-ticker.C:
		case <-mgr.stop:
			break loop
		}
	}

	if mgr.cmd != nil {
		mgr.cmd.Close()
		mgr.cmd = nil
	}
	log.Logf(0, "%v: stopped", mgr.name)
}

func (mgr *Manager) pollAndBuild(lastCommit string, latestInfo *BuildInfo) (
	string, *BuildInfo, time.Duration) {
	rebuildAfter := buildRetryPeriod
	commit, err := mgr.repo.Poll(mgr.mgrcfg.Repo, mgr.mgrcfg.Branch)
	if err != nil {
		mgr.Errorf("failed to poll: %v", err)
	} else {
		log.Logf(0, "%v: poll: %v", mgr.name, commit.Hash)
		if commit.Hash != lastCommit &&
			(latestInfo == nil ||
				commit.Hash != latestInfo.KernelCommit ||
				mgr.configTag != latestInfo.KernelConfigTag) {
			lastCommit = commit.Hash
			select {
			case <-buildSem.WaitC():
				log.Logf(0, "%v: building kernel...", mgr.name)
				if err := mgr.build(commit); err != nil {
					log.Logf(0, "%v: %v", mgr.name, err)
				} else {
					log.Logf(0, "%v: build successful, [re]starting manager", mgr.name)
					rebuildAfter = kernelRebuildPeriod
					latestInfo = mgr.checkLatest()
					if latestInfo == nil {
						mgr.Errorf("failed to read build info after build")
					}
				}
				buildSem.Signal()
			case <-mgr.stop:
			}
		}
	}
	return lastCommit, latestInfo, rebuildAfter
}

// BuildInfo characterizes a kernel build.
type BuildInfo struct {
	Time              time.Time // when the build was done
	Tag               string    // unique tag combined from compiler id, kernel commit and config tag
	CompilerID        string    // compiler identity string (e.g. "gcc 7.1.1")
	KernelRepo        string
	KernelBranch      string
	KernelCommit      string // git hash of kernel checkout
	KernelCommitTitle string
	KernelCommitDate  time.Time
	KernelConfigTag   string // SHA1 hash of .config contents
}

func loadBuildInfo(dir string) (*BuildInfo, error) {
	info := new(BuildInfo)
	if err := config.LoadFile(filepath.Join(dir, "tag"), info); err != nil {
		return nil, err
	}
	return info, nil
}

// checkLatest checks if we have a good working latest build and returns its build info.
// If the build is missing/broken, nil is returned.
func (mgr *Manager) checkLatest() *BuildInfo {
	if !osutil.FilesExist(mgr.latestDir, imageFiles) {
		return nil
	}
	info, _ := loadBuildInfo(mgr.latestDir)
	return info
}

func (mgr *Manager) createBuildInfo(kernelCommit *vcs.Commit, compilerID string) *BuildInfo {
	var tagData []byte
	tagData = append(tagData, mgr.name...)
	tagData = append(tagData, kernelCommit.Hash...)
	tagData = append(tagData, compilerID...)
	tagData = append(tagData, mgr.configTag...)
	return &BuildInfo{
		Time:              time.Now(),
		Tag:               hash.String(tagData),
		CompilerID:        compilerID,
		KernelRepo:        mgr.mgrcfg.Repo,
		KernelBranch:      mgr.mgrcfg.Branch,
		KernelCommit:      kernelCommit.Hash,
		KernelCommitTitle: kernelCommit.Title,
		KernelCommitDate:  kernelCommit.CommitDate,
		KernelConfigTag:   mgr.configTag,
	}
}

func (mgr *Manager) build(kernelCommit *vcs.Commit) error {
	// We first form the whole image in tmp dir and then rename it to latest.
	tmpDir := mgr.latestDir + ".tmp"
	if err := os.RemoveAll(tmpDir); err != nil {
		return fmt.Errorf("failed to remove tmp dir: %v", err)
	}
	if err := osutil.MkdirAll(tmpDir); err != nil {
		return fmt.Errorf("failed to create tmp dir: %v", err)
	}
	params := build.Params{
		TargetOS:     mgr.managercfg.TargetOS,
		TargetArch:   mgr.managercfg.TargetVMArch,
		VMType:       mgr.managercfg.Type,
		KernelDir:    mgr.kernelDir,
		OutputDir:    tmpDir,
		Compiler:     mgr.mgrcfg.Compiler,
		Linker:       mgr.mgrcfg.Linker,
		Ccache:       mgr.mgrcfg.Ccache,
		UserspaceDir: mgr.mgrcfg.Userspace,
		CmdlineFile:  mgr.mgrcfg.KernelCmdline,
		SysctlFile:   mgr.mgrcfg.KernelSysctl,
		Config:       mgr.configData,
		Build:        mgr.mgrcfg.Build,
	}
	details, err := build.Image(params)
	info := mgr.createBuildInfo(kernelCommit, details.CompilerID)
	if err != nil {
		rep := &report.Report{
			Title: fmt.Sprintf("%v build error", mgr.mgrcfg.RepoAlias),
		}
		switch err1 := err.(type) {
		case *build.KernelError:
			rep.Report = err1.Report
			rep.Output = err1.Output
			rep.Recipients = err1.Recipients
		case *osutil.VerboseError:
			rep.Report = []byte(err1.Title)
			rep.Output = err1.Output
		default:
			rep.Report = []byte(err.Error())
		}
		if err := mgr.reportBuildError(rep, info, tmpDir); err != nil {
			mgr.Errorf("failed to report image error: %v", err)
		}
		return fmt.Errorf("kernel build failed: %v", err)
	}

	if err := config.SaveFile(filepath.Join(tmpDir, "tag"), info); err != nil {
		return fmt.Errorf("failed to write tag file: %v", err)
	}

	if err := mgr.testImage(tmpDir, info); err != nil {
		return err
	}

	// Now try to replace latest with our tmp dir as atomically as we can get on Linux.
	if err := os.RemoveAll(mgr.latestDir); err != nil {
		return fmt.Errorf("failed to remove latest dir: %v", err)
	}
	return osutil.Rename(tmpDir, mgr.latestDir)
}

func (mgr *Manager) restartManager() {
	if !osutil.FilesExist(mgr.latestDir, imageFiles) {
		mgr.Errorf("can't start manager, image files missing")
		return
	}
	if mgr.cmd != nil {
		mgr.cmd.Close()
		mgr.cmd = nil
	}
	if err := osutil.LinkFiles(mgr.latestDir, mgr.currentDir, imageFiles); err != nil {
		mgr.Errorf("failed to create current image dir: %v", err)
		return
	}
	info, err := loadBuildInfo(mgr.currentDir)
	if err != nil {
		mgr.Errorf("failed to load build info: %v", err)
		return
	}
	buildTag, err := mgr.uploadBuild(info, mgr.currentDir)
	if err != nil {
		mgr.Errorf("failed to upload build: %v", err)
		return
	}
	cfgFile, err := mgr.writeConfig(buildTag)
	if err != nil {
		mgr.Errorf("failed to create manager config: %v", err)
		return
	}
	bin := filepath.FromSlash("syzkaller/current/bin/syz-manager")
	logFile := filepath.Join(mgr.currentDir, "manager.log")
	args := []string{"-config", cfgFile}
	if mgr.debug {
		args = append(args, "-debug")
	}
	mgr.cmd = NewManagerCmd(mgr.name, logFile, mgr.Errorf, bin, args...)
}

func (mgr *Manager) testImage(imageDir string, info *BuildInfo) error {
	log.Logf(0, "%v: testing image...", mgr.name)
	mgrcfg, err := mgr.createTestConfig(imageDir, info)
	if err != nil {
		return fmt.Errorf("failed to create manager config: %v", err)
	}
	defer os.RemoveAll(mgrcfg.Workdir)
	if !vm.AllowsOvercommit(mgrcfg.Type) {
		return nil // No support for creating machines out of thin air.
	}
	env, err := instance.NewEnv(mgrcfg, buildSem, testSem)
	if err != nil {
		return err
	}
	const (
		testVMs     = 3
		maxFailures = 1
	)
	results, err := env.Test(testVMs, nil, nil, nil)
	if err != nil {
		return err
	}
	failures := 0
	var failureErr error
	for _, res := range results {
		if res.Error == nil {
			continue
		}
		failures++
		switch err := res.Error.(type) {
		case *instance.TestError:
			if rep := err.Report; rep != nil {
				what := "test"
				if err.Boot {
					what = "boot"
				}
				rep.Title = fmt.Sprintf("%v %v error: %v",
					mgr.mgrcfg.RepoAlias, what, rep.Title)
				// There are usually no duplicates for boot errors, so we reset AltTitles.
				// But if we pass them, we would need to add the same prefix as for Title
				// in order to avoid duping boot bugs with non-boot bugs.
				rep.AltTitles = nil
				if err := mgr.reportBuildError(rep, info, imageDir); err != nil {
					mgr.Errorf("failed to report image error: %v", err)
				}
			}
			if err.Boot {
				failureErr = fmt.Errorf("VM boot failed with: %v", err)
			} else {
				failureErr = fmt.Errorf("VM testing failed with: %v", err)
			}
		default:
			failureErr = res.Error
		}
	}
	if failures > maxFailures {
		return failureErr
	}
	return nil
}

func (mgr *Manager) reportBuildError(rep *report.Report, info *BuildInfo, imageDir string) error {
	if mgr.dash == nil {
		log.Logf(0, "%v: image testing failed: %v\n\n%s\n\n%s",
			mgr.name, rep.Title, rep.Report, rep.Output)
		return nil
	}
	build, err := mgr.createDashboardBuild(info, imageDir, "error")
	if err != nil {
		return err
	}
	if mgr.storage != nil {
		// We have to send assets together with the other info because the report
		// might be generated immediately.
		uploadedAssets, err := mgr.uploadBuildAssets(build, imageDir)
		if err == nil {
			build.Assets = uploadedAssets
		} else {
			log.Logf(0, "%v: failed to upload build assets: %s", mgr.name, err)
		}
	}
	req := &dashapi.BuildErrorReq{
		Build: *build,
		Crash: dashapi.Crash{
			Title:      rep.Title,
			AltTitles:  rep.AltTitles,
			Corrupted:  false, // Otherwise they get merged with other corrupted reports.
			Recipients: rep.Recipients.ToDash(),
			Log:        rep.Output,
			Report:     rep.Report,
		},
	}
	if rep.GuiltyFile != "" {
		req.Crash.GuiltyFiles = []string{rep.GuiltyFile}
	}
	if err := mgr.dash.ReportBuildError(req); err != nil {
		return err
	}
	return nil
}

func (mgr *Manager) createTestConfig(imageDir string, info *BuildInfo) (*mgrconfig.Config, error) {
	mgrcfg := new(mgrconfig.Config)
	*mgrcfg = *mgr.managercfg
	mgrcfg.Name += "-test"
	mgrcfg.Tag = info.KernelCommit
	mgrcfg.Workdir = filepath.Join(imageDir, "workdir")
	if err := instance.SetConfigImage(mgrcfg, imageDir, true); err != nil {
		return nil, err
	}
	mgrcfg.KernelSrc = mgr.kernelDir
	if err := mgrconfig.Complete(mgrcfg); err != nil {
		return nil, fmt.Errorf("bad manager config: %v", err)
	}
	return mgrcfg, nil
}

func (mgr *Manager) writeConfig(buildTag string) (string, error) {
	mgrcfg := new(mgrconfig.Config)
	*mgrcfg = *mgr.managercfg

	if mgr.dash != nil {
		mgrcfg.DashboardClient = mgr.dash.Client
		mgrcfg.DashboardAddr = mgr.dash.Addr
		mgrcfg.DashboardKey = mgr.dash.Key
		mgrcfg.AssetStorage = mgr.cfg.AssetStorage
	}
	if mgr.cfg.HubAddr != "" {
		mgrcfg.HubClient = mgr.cfg.Name
		mgrcfg.HubAddr = mgr.cfg.HubAddr
		mgrcfg.HubKey = mgr.cfg.HubKey
	}
	mgrcfg.Tag = buildTag
	mgrcfg.Workdir = mgr.workDir
	// There's not much point in keeping disabled progs in the syz-ci corpuses.
	// If the syscalls on some instance are enabled again, syz-hub will provide
	// it with the missing progs over time.
	// And, on the other hand, PreserveCorpus=false lets us disable syscalls in
	// the least destructive way for the rest of the corpus - calls will be cut
	// out the of programs and the leftovers will be retriaged.
	mgrcfg.PreserveCorpus = false
	if err := instance.SetConfigImage(mgrcfg, mgr.currentDir, false); err != nil {
		return "", err
	}
	// Strictly saying this is somewhat racy as builder can concurrently
	// update the source, or even delete and re-clone. If this causes
	// problems, we need to make a copy of sources after build.
	mgrcfg.KernelSrc = mgr.kernelDir
	if err := mgrconfig.Complete(mgrcfg); err != nil {
		return "", fmt.Errorf("bad manager config: %v", err)
	}
	configFile := filepath.Join(mgr.currentDir, "manager.cfg")
	if err := config.SaveFile(configFile, mgrcfg); err != nil {
		return "", err
	}
	return configFile, nil
}

func (mgr *Manager) uploadBuild(info *BuildInfo, imageDir string) (string, error) {
	if mgr.dash == nil {
		// Dashboard identifies builds by unique tags that are combined
		// from kernel tag, compiler tag and config tag.
		// This combined tag is meaningless without dashboard,
		// so we use kenrel tag (commit tag) because it communicates
		// at least some useful information.
		return info.KernelCommit, nil
	}

	build, err := mgr.createDashboardBuild(info, imageDir, "normal")
	if err != nil {
		return "", err
	}
	mgr.lastBuild = build
	commitTitles, fixCommits, err := mgr.pollCommits(info.KernelCommit)
	if err != nil {
		// This is not critical for operation.
		mgr.Errorf("failed to poll commits: %v", err)
	}
	build.Commits = commitTitles
	build.FixCommits = fixCommits
	if mgr.storage != nil {
		// We always upload build assets -- we create a separate Build object not just for
		// different kernel commits, but also for different syzkaller commits, configs, etc.
		// Since we deduplicate assets by hashing, this should not be a problem -- no assets
		// will be actually duplicated, only the records in the DB.
		assets, err := mgr.uploadBuildAssets(build, imageDir)
		if err != nil {
			mgr.Errorf("failed to upload build assets: %v", err)
			return "", err
		}
		build.Assets = assets
	}
	if err := mgr.dash.UploadBuild(build); err != nil {
		return "", err
	}
	return build.ID, nil
}

func (mgr *Manager) createDashboardBuild(info *BuildInfo, imageDir, typ string) (*dashapi.Build, error) {
	var kernelConfig []byte
	if kernelConfigFile := filepath.Join(imageDir, "kernel.config"); osutil.IsExist(kernelConfigFile) {
		var err error
		if kernelConfig, err = os.ReadFile(kernelConfigFile); err != nil {
			return nil, fmt.Errorf("failed to read kernel.config: %v", err)
		}
	}
	// Resulting build depends on both kernel build tag and syzkaller commmit.
	// Also mix in build type, so that image error builds are not merged into normal builds.
	var tagData []byte
	tagData = append(tagData, info.Tag...)
	tagData = append(tagData, prog.GitRevisionBase...)
	tagData = append(tagData, typ...)
	build := &dashapi.Build{
		Manager:             mgr.name,
		ID:                  hash.String(tagData),
		OS:                  mgr.managercfg.TargetOS,
		Arch:                mgr.managercfg.TargetArch,
		VMArch:              mgr.managercfg.TargetVMArch,
		SyzkallerCommit:     prog.GitRevisionBase,
		SyzkallerCommitDate: prog.GitRevisionDate,
		CompilerID:          info.CompilerID,
		KernelRepo:          info.KernelRepo,
		KernelBranch:        info.KernelBranch,
		KernelCommit:        info.KernelCommit,
		KernelCommitTitle:   info.KernelCommitTitle,
		KernelCommitDate:    info.KernelCommitDate,
		KernelConfig:        kernelConfig,
	}
	return build, nil
}

// pollCommits asks dashboard what commits it is interested in (i.e. fixes for
// open bugs) and returns subset of these commits that are present in a build
// on commit buildCommit.
func (mgr *Manager) pollCommits(buildCommit string) ([]string, []dashapi.Commit, error) {
	resp, err := mgr.dash.BuilderPoll(mgr.name)
	if err != nil || len(resp.PendingCommits) == 0 && resp.ReportEmail == "" {
		return nil, nil, err
	}
	var present []string
	if len(resp.PendingCommits) != 0 {
		commits, err := mgr.repo.ListRecentCommits(buildCommit)
		if err != nil {
			return nil, nil, err
		}
		m := make(map[string]bool, len(commits))
		for _, com := range commits {
			m[vcs.CanonicalizeCommit(com)] = true
		}
		for _, com := range resp.PendingCommits {
			if m[vcs.CanonicalizeCommit(com)] {
				present = append(present, com)
			}
		}
	}
	var fixCommits []dashapi.Commit
	if resp.ReportEmail != "" {
		if !brokenRepo(mgr.mgrcfg.Repo) {
			commits, err := mgr.repo.ExtractFixTagsFromCommits(buildCommit, resp.ReportEmail)
			if err != nil {
				return nil, nil, err
			}
			for _, com := range commits {
				fixCommits = append(fixCommits, dashapi.Commit{
					Title:  com.Title,
					BugIDs: com.Tags,
					Date:   com.Date,
				})
			}
		}
	}
	return present, fixCommits, nil
}

func (mgr *Manager) uploadBuildAssets(buildInfo *dashapi.Build, assetFolder string) ([]dashapi.NewAsset, error) {
	if mgr.storage == nil {
		// No reason to continue anyway.
		return nil, fmt.Errorf("asset storage is not configured")
	}
	type pendingAsset struct {
		path      string
		assetType dashapi.AssetType
		name      string
	}
	pending := []pendingAsset{}
	kernelFile := filepath.Join(assetFolder, "kernel")
	if osutil.IsExist(kernelFile) {
		fileName := "kernel"
		if buildInfo.OS == targets.Linux {
			fileName = path.Base(build.LinuxKernelImage(buildInfo.Arch))
		}
		pending = append(pending, pendingAsset{kernelFile, dashapi.KernelImage, fileName})
	}
	imageFile := filepath.Join(assetFolder, "image")
	if osutil.IsExist(imageFile) {
		if mgr.managercfg.Type == "qemu" {
			// For qemu we currently use non-bootable disk images.
			pending = append(pending, pendingAsset{imageFile, dashapi.NonBootableDisk,
				"non_bootable_disk.raw"})
		} else {
			pending = append(pending, pendingAsset{imageFile, dashapi.BootableDisk,
				"disk.raw"})
		}
	}
	target := mgr.managercfg.SysTarget
	kernelObjFile := filepath.Join(assetFolder, "obj", target.KernelObject)
	if osutil.IsExist(kernelObjFile) {
		pending = append(pending,
			pendingAsset{kernelObjFile, dashapi.KernelObject, target.KernelObject})
	}
	// TODO: add initrd?
	ret := []dashapi.NewAsset{}
	for _, pendingAsset := range pending {
		if !mgr.storage.AssetTypeEnabled(pendingAsset.assetType) {
			continue
		}
		file, err := os.Open(pendingAsset.path)
		if err != nil {
			log.Logf(0, "failed to open an asset for uploading: %s, %s",
				pendingAsset.path, err)
			continue
		}
		if mgr.debugStorage {
			log.Logf(0, "uploading an asset %s of type %s",
				pendingAsset.path, pendingAsset.assetType)
		}
		extra := &asset.ExtraUploadArg{SkipIfExists: true}
		hash := sha256.New()
		if _, err := io.Copy(hash, file); err != nil {
			log.Logf(0, "failed calculate hash for the asset %s: %s", pendingAsset.path, err)
			continue
		}
		extra.UniqueTag = fmt.Sprintf("%x", hash.Sum(nil))
		// Now we need to go back to the beginning of the file again.
		if _, err := file.Seek(0, io.SeekStart); err != nil {
			log.Logf(0, "failed wind back the opened file for %s: %s", pendingAsset.path, err)
			continue
		}
		info, err := mgr.storage.UploadBuildAsset(file, pendingAsset.name,
			pendingAsset.assetType, buildInfo, extra)
		if err != nil {
			log.Logf(0, "failed to upload an asset: %s, %s",
				pendingAsset.path, err)
			continue
		} else if mgr.debugStorage {
			log.Logf(0, "uploaded an asset: %#v", info)
		}
		ret = append(ret, info)
	}
	return ret, nil
}

func (mgr *Manager) uploadCoverReport() error {
	directUpload := mgr.managercfg.Cover && mgr.cfg.CoverUploadPath != ""
	if mgr.storage == nil && !directUpload {
		// Cover report uploading is disabled.
		return nil
	}
	if mgr.storage != nil && directUpload {
		return fmt.Errorf("cover report must be either uploaded directly or via asset storage")
	}
	// Report generation can consume lots of memory. Generate one at a time.
	select {
	case <-buildSem.WaitC():
	case <-mgr.stop:
		return nil
	}
	defer buildSem.Signal()

	// Get coverage report from manager.
	addr := mgr.managercfg.HTTP
	if addr != "" && addr[0] == ':' {
		addr = "127.0.0.1" + addr // in case addr is ":port"
	}
	client := http.Client{
		Timeout: time.Hour,
	}
	resp, err := client.Get(fmt.Sprintf("http://%v/cover", addr))
	if err != nil {
		return fmt.Errorf("failed to get report: %v", err)
	}
	defer resp.Body.Close()
	if directUpload {
		return mgr.uploadFile(mgr.cfg.CoverUploadPath, mgr.name+".html", resp.Body)
	}
	// Upload via the asset storage.
	newAsset, err := mgr.storage.UploadBuildAsset(resp.Body, mgr.name+".html",
		dashapi.HTMLCoverageReport, mgr.lastBuild, nil)
	if err != nil {
		return fmt.Errorf("failed to upload html coverage report: %w", err)
	}
	err = mgr.storage.ReportBuildAssets(mgr.lastBuild, newAsset)
	if err != nil {
		return fmt.Errorf("failed to report the html coverage report asset: %w", err)
	}
	return nil
}

func (mgr *Manager) uploadCorpus() error {
	f, err := os.Open(filepath.Join(mgr.workDir, "corpus.db"))
	if err != nil {
		return err
	}
	defer f.Close()
	return mgr.uploadFile(mgr.cfg.CorpusUploadPath, mgr.name+"-corpus.db", f)
}

func (mgr *Manager) uploadFile(dstPath, name string, file io.Reader) error {
	URL, err := url.Parse(dstPath)
	if err != nil {
		return fmt.Errorf("failed to parse upload path: %v", err)
	}
	URL.Path = path.Join(URL.Path, name)
	URLStr := URL.String()
	log.Logf(0, "uploading %v to %v", name, URLStr)
	if strings.HasPrefix(URLStr, "gs://") {
		return uploadFileGCS(strings.TrimPrefix(URLStr, "gs://"), file, mgr.cfg.PublishGCS)
	}
	if strings.HasPrefix(URLStr, "http://") ||
		strings.HasPrefix(URLStr, "https://") {
		return uploadFileHTTPPut(URLStr, file)
	}
	// Use GCS as default to maintain backwards compatibility.
	return uploadFileGCS(URLStr, file, mgr.cfg.PublishGCS)
}

func uploadFileGCS(URL string, file io.Reader, publish bool) error {
	GCS, err := gcs.NewClient()
	if err != nil {
		return fmt.Errorf("failed to create GCS client: %v", err)
	}
	defer GCS.Close()
	gcsWriter, err := GCS.FileWriter(URL)
	if err != nil {
		return fmt.Errorf("failed to create GCS writer: %v", err)
	}
	if _, err := io.Copy(gcsWriter, file); err != nil {
		gcsWriter.Close()
		return fmt.Errorf("failed to copy report: %v", err)
	}
	if err := gcsWriter.Close(); err != nil {
		return fmt.Errorf("failed to close gcs writer: %v", err)
	}
	if publish {
		return GCS.Publish(URL)
	}
	return nil
}

func uploadFileHTTPPut(URL string, file io.Reader) error {
	req, err := http.NewRequest(http.MethodPut, URL, file)
	if err != nil {
		return fmt.Errorf("failed to create HTTP PUT request: %v", err)
	}
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("failed to perform HTTP PUT request: %v", err)
	}
	defer resp.Body.Close()
	if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
		return fmt.Errorf("HTTP PUT failed with status code: %v", resp.StatusCode)
	}
	return nil
}

// Errorf logs non-fatal error and sends it to dashboard.
func (mgr *Manager) Errorf(msg string, args ...interface{}) {
	log.Errorf(mgr.name+": "+msg, args...)
	if mgr.dash != nil {
		mgr.dash.LogError(mgr.name, msg, args...)
	}
}
