// Copyright 2022 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 cuttlefish allows to use Cuttlefish Android emulators hosted on Google Compute Engine
// (GCE) virtual machines as VMs. It is assumed that syz-manager also runs on GCE as VMs are
// created in the current project/zone.
//
// See https://cloud.google.com/compute/docs for details.
// In particular, how to build GCE-compatible images:
// https://cloud.google.com/compute/docs/tutorials/building-images
package cuttlefish

import (
	"fmt"
	"os/exec"
	"path/filepath"
	"time"

	"github.com/google/syzkaller/pkg/osutil"
	"github.com/google/syzkaller/pkg/report"
	"github.com/google/syzkaller/vm/gce"
	"github.com/google/syzkaller/vm/vmimpl"
)

const (
	deviceRoot     = "/data/fuzz"
	consoleReadCmd = "tail -f cuttlefish/instances/cvd-1/kernel.log"
)

func init() {
	vmimpl.Register("cuttlefish", ctor, true)
}

type Pool struct {
	env     *vmimpl.Env
	gcePool *gce.Pool
}

type instance struct {
	name    string
	sshKey  string
	sshUser string
	debug   bool
	gceInst vmimpl.Instance
}

func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
	gcePool, err := gce.Ctor(env, consoleReadCmd)
	if err != nil {
		return nil, fmt.Errorf("failed to create underlying GCE pool: %s", err)
	}

	pool := &Pool{
		env:     env,
		gcePool: gcePool,
	}

	return pool, nil
}

func (pool *Pool) Count() int {
	return pool.gcePool.Count()
}

func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) {
	gceInst, err := pool.gcePool.Create(workdir, index)
	if err != nil {
		return nil, fmt.Errorf("failed to create underlying gce instance: %s", err)
	}

	inst := &instance{
		name:    fmt.Sprintf("%v-%v", pool.env.Name, index),
		sshKey:  pool.env.SSHKey,
		sshUser: pool.env.SSHUser,
		debug:   pool.env.Debug,
		gceInst: gceInst,
	}

	// Start a Cuttlefish device on the GCE instance.
	if err := inst.runOnHost(10*time.Minute,
		fmt.Sprintf("./bin/launch_cvd -daemon -kernel_path=./bzImage -initramfs_path=./initramfs.img"+
			" --noenable_sandbox -report_anonymous_usage_stats=n --memory_mb=8192")); err != nil {
		return nil, fmt.Errorf("failed to start cuttlefish: %s", err)
	}

	if err := inst.runOnHost(10*time.Minute, "adb wait-for-device"); err != nil {
		return nil, fmt.Errorf("failed while waiting for device: %s", err)
	}

	if err := inst.runOnHost(5*time.Minute, "adb root"); err != nil {
		return nil, fmt.Errorf("failed to get root access to device: %s", err)
	}

	if err := inst.runOnHost(5*time.Minute, fmt.Sprintf("adb shell '"+
		"setprop persist.dbg.keep_debugfs_mounted 1;"+
		"mount -t debugfs debugfs /sys/kernel/debug;"+
		"chmod 0755 /sys/kernel/debug;"+
		"mkdir %s;"+
		"'", deviceRoot)); err != nil {
		return nil, fmt.Errorf("failed to mount debugfs to /sys/kernel/debug: %s", err)
	}

	return inst, nil
}

func (inst *instance) sshArgs(command string) []string {
	sshArgs := append(vmimpl.SSHArgs(inst.debug, inst.sshKey, 22), inst.sshUser+"@"+inst.name)
	if inst.sshUser != "root" {
		return append(sshArgs, "sudo", "bash", "-c", "'"+command+"'")
	}
	return append(sshArgs, command)
}

func (inst *instance) runOnHost(timeout time.Duration, command string) error {
	_, err := osutil.RunCmd(timeout, "/root", "ssh", inst.sshArgs(command)...)

	return err
}

func (inst *instance) Copy(hostSrc string) (string, error) {
	gceDst, err := inst.gceInst.Copy(hostSrc)
	if err != nil {
		return "", fmt.Errorf("error copying to worker instance: %s", err)
	}

	deviceDst := filepath.Join(deviceRoot, filepath.Base(hostSrc))
	pushCmd := fmt.Sprintf("adb push %s %s", gceDst, deviceDst)

	if err := inst.runOnHost(5*time.Minute, pushCmd); err != nil {
		return "", fmt.Errorf("error pushing to device: %s", err)
	}

	return deviceDst, nil
}

func (inst *instance) Forward(port int) (string, error) {
	hostForward, err := inst.gceInst.Forward(port)
	if err != nil {
		return "", fmt.Errorf("failed to get IP/port from GCE instance: %s", err)
	}

	// Run socat in the background. This hangs when run from runOnHost().
	cmdStr := fmt.Sprintf("nohup socat TCP-LISTEN:%d,fork TCP:%s", port, hostForward)
	cmdArgs := append([]string{"-f"}, inst.sshArgs(cmdStr)...)
	cmd := exec.Command("ssh", cmdArgs...)
	cmd.Dir = "/root"
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("unable to forward port on host: %s", err)
	}

	for i := 0; i < 100; i++ {
		devicePort := vmimpl.RandomPort()
		cmd := fmt.Sprintf("adb reverse tcp:%d tcp:%d", devicePort, port)
		err = inst.runOnHost(10*time.Second, cmd)
		if err == nil {
			return fmt.Sprintf("127.0.0.1:%d", devicePort), nil
		}
	}

	return "", fmt.Errorf("unable to forward port on device: %s", err)
}

func (inst *instance) Close() {
	inst.gceInst.Close()
}

func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) (
	<-chan []byte, <-chan error, error) {
	return inst.gceInst.Run(timeout, stop, fmt.Sprintf("adb shell 'cd %s; %s'", deviceRoot, command))
}

func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) {
	return nil, false
}
