package main

import (
	"fmt"
	"net"
	"os"
	"os/signal"
	"strings"
	"syscall"

	"google.golang.org/grpc"

	"github.com/Sirupsen/logrus"
	runc "github.com/crosbymichael/go-runc"
	"github.com/docker/containerd"
	shimapi "github.com/docker/containerd/api/services/shim"
	"github.com/docker/containerd/linux/shim"
	"github.com/docker/containerd/reaper"
	"github.com/docker/containerd/sys"
	"github.com/urfave/cli"
)

const usage = `
                    __        _                     __           __    _         
  _________  ____  / /_____ _(_)___  ___  _________/ /     _____/ /_  (_)___ ___ 
 / ___/ __ \/ __ \/ __/ __ ` + "`" + `/ / __ \/ _ \/ ___/ __  /_____/ ___/ __ \/ / __ ` + "`" + `__ \
/ /__/ /_/ / / / / /_/ /_/ / / / / /  __/ /  / /_/ /_____(__  ) / / / / / / / / /
\___/\____/_/ /_/\__/\__,_/_/_/ /_/\___/_/   \__,_/     /____/_/ /_/_/_/ /_/ /_/ 
                                                                                 
shim for container lifecycle and reconnection
`

func main() {
	app := cli.NewApp()
	app.Name = "containerd-shim"
	app.Version = containerd.Version
	app.Usage = usage
	app.Flags = []cli.Flag{
		cli.BoolFlag{
			Name:  "debug",
			Usage: "enable debug output in logs",
		},
	}
	app.Before = func(context *cli.Context) error {
		if context.GlobalBool("debug") {
			logrus.SetLevel(logrus.DebugLevel)
		}
		return nil
	}
	app.Action = func(context *cli.Context) error {
		// start handling signals as soon as possible so that things are properly reaped
		// or if runtime exits before we hit the handler
		signals, err := setupSignals()
		if err != nil {
			return err
		}
		if err := setupRoot(); err != nil {
			return err
		}
		var (
			server = grpc.NewServer()
			sv     = shim.New()
		)
		logrus.Debug("registering grpc server")
		shimapi.RegisterShimServer(server, sv)
		if err := serve(server, "shim.sock"); err != nil {
			return err
		}
		return handleSignals(signals, server, sv)
	}
	if err := app.Run(os.Args); err != nil {
		fmt.Fprintf(os.Stderr, "containerd-shim: %s\n", err)
		os.Exit(1)
	}
}

// setupSignals creates a new signal handler for all signals and sets the shim as a
// sub-reaper so that the container processes are reparented
func setupSignals() (chan os.Signal, error) {
	signals := make(chan os.Signal, 2048)
	signal.Notify(signals)
	// make sure runc is setup to use the monitor
	// for waiting on processes
	runc.Monitor = reaper.Default
	// set the shim as the subreaper for all orphaned processes created by the container
	if err := sys.SetSubreaper(1); err != nil {
		return nil, err
	}
	return signals, nil
}

// serve serves the grpc API over a unix socket at the provided path
// this function does not block
func serve(server *grpc.Server, path string) error {
	l, err := net.FileListener(os.NewFile(3, "socket"))
	if err != nil {
		return err
	}
	logrus.WithField("socket", path).Debug("serving api on unix socket")
	go func() {
		defer l.Close()
		if err := server.Serve(l); err != nil &&
			!strings.Contains(err.Error(), "use of closed network connection") {
			logrus.WithError(err).Fatal("containerd-shim: GRPC server failure")
		}
	}()
	return nil
}

func handleSignals(signals chan os.Signal, server *grpc.Server, service *shim.Service) error {
	for s := range signals {
		logrus.WithField("signal", s).Debug("received signal")
		switch s {
		case syscall.SIGCHLD:
			exits, err := reaper.Reap()
			if err != nil {
				logrus.WithError(err).Error("reap exit status")
			}
			for _, e := range exits {
				logrus.WithFields(logrus.Fields{
					"status": e.Status,
					"pid":    e.Pid,
				}).Debug("process exited")
				if err := service.ProcessExit(e); err != nil {
					return err
				}
			}
		case syscall.SIGTERM, syscall.SIGINT:
			// TODO: should we forward signals to the processes if they are still running?
			// i.e. machine reboot
			server.Stop()
			return nil
		}
	}
	return nil
}

// setupRoot sets up the root as the shim is started in its own mount namespace
func setupRoot() error {
	return syscall.Mount("", "/", "", syscall.MS_SLAVE|syscall.MS_REC, "")
}
