// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package proclib

import (
	"fmt"
	"sync"
)

// Task records the unique coordinates of a task from the perspective of a Nomad
// client running the task, that is to say (alloc_id, task_name). Also indicates
// whether the task is making use of reserved cpu cores.
type Task struct {
	AllocID string
	Task    string
	Cores   bool
}

func (task Task) String() string {
	return fmt.Sprintf("%s/%s", task.AllocID[0:8], task.Task)
}

type create func(Task) ProcessWrangler

// Wranglers keeps track of the ProcessWrangler created for each task. Some
// operating systems may implement ProcessWranglers to ensure that all of the
// processes created by a Task are killed, going a step beyond trusting the
// task drivers to properly clean things up. (Well, on Linux anyway.)
//
// This state must be restored on Client agent startup.
type Wranglers struct {
	configs *Configs
	create  create

	lock sync.Mutex
	m    map[Task]ProcessWrangler
}

// Setup any process management technique relevant to the operating system and
// its particular configuration.
func (w *Wranglers) Setup(task Task) error {
	w.configs.Logger.Trace("setup client process management", "task", task)

	// create process wrangler for task
	pw := w.create(task)

	// perform any initialization if necessary (e.g. create cgroup)
	// if this doesn't work just keep going; it's up to each task driver
	// implementation to decide if this is a failure mode
	_ = pw.Initialize()

	w.lock.Lock()
	defer w.lock.Unlock()

	// keep track of the process wrangler for task
	w.m[task] = pw

	return nil
}

// Destroy any processes still running that were spawned by task. Ideally the
// task driver should be implemented well enough for this to not be necessary,
// but we protect the Client as best we can regardless.
//
// Note that this is called from a TR.Stop which must be idempotent.
func (w *Wranglers) Destroy(task Task) error {
	w.configs.Logger.Trace("destroy and cleanup remnant task processes", "task", task)

	w.lock.Lock()
	defer w.lock.Unlock()

	if pw, exists := w.m[task]; exists {
		pw.Kill()
		pw.Cleanup()
		delete(w.m, task)
	}

	return nil
}

// A ProcessWrangler "owns" a particular Task on a client, enabling the client
// to kill and cleanup processes created by that Task, without help from the
// task driver. Currently we have implementations only for Linux (via cgroups).
type ProcessWrangler interface {
	Initialize() error
	Kill() error
	Cleanup() error
}
