isle/go-workspace/src/env.go

253 lines
6.4 KiB
Go
Raw Normal View History

package crypticnet
import (
"context"
"cryptic-net/tarutil"
"cryptic-net/yamlutil"
"errors"
"fmt"
"io/fs"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
"github.com/adrg/xdg"
)
// Names of various environment variables which get set by the entrypoint.
const (
DaemonYmlPathEnvVar = "_DAEMON_YML_PATH"
BootstrapPathEnvVar = "_BOOTSTRAP_PATH"
RuntimeDirPathEnvVar = "_RUNTIME_DIR_PATH"
DataDirPathEnvVar = "_DATA_DIR_PATH"
)
// Env contains the values of environment variables, as well as other entities
// which are useful across all processes.
type Env struct {
Context context.Context
AppDirPath string
DaemonYmlPath string
RuntimeDirPath string
DataDirPath string
// If NewEnv is called with bootstrapOptional, and a bootstrap file is not
// found, then these fields will not be set.
BootstrapPath string
BootstrapFS fs.FS
Hosts map[string]Host
HostName string
thisDaemon DaemonYml
thisDaemonOnce sync.Once
}
func getAppDirPath() string {
appDirPath := os.Getenv("APPDIR")
if appDirPath == "" {
appDirPath = "."
}
return appDirPath
}
// NewEnv calculates an Env instance based on the APPDIR and XDG envvars.
//
// If bootstrapOptional is true then NewEnv will first check if a bootstrap file
// can be found in the expected places, and if not then it will not populate
// BootstrapFS or any other fields based on it.
func NewEnv(bootstrapOptional bool) (*Env, error) {
runtimeDirPath := filepath.Join(xdg.RuntimeDir, "cryptic-net")
appDirPath := getAppDirPath()
env := &Env{
AppDirPath: appDirPath,
DaemonYmlPath: filepath.Join(runtimeDirPath, "daemon.yml"),
RuntimeDirPath: runtimeDirPath,
DataDirPath: filepath.Join(xdg.DataHome, "cryptic-net"),
}
return env, env.init(bootstrapOptional)
}
// ReadEnv reads an Env from the process's environment variables, rather than
// calculating like NewEnv does.
func ReadEnv() (*Env, error) {
var err error
readEnv := func(key string) string {
if err != nil {
return ""
}
val := os.Getenv(key)
if val == "" {
err = fmt.Errorf("envvar %q not set", key)
}
return val
}
env := &Env{
AppDirPath: getAppDirPath(),
DaemonYmlPath: readEnv(DaemonYmlPathEnvVar),
RuntimeDirPath: readEnv(RuntimeDirPathEnvVar),
DataDirPath: readEnv(DataDirPathEnvVar),
}
if err != nil {
return nil, err
}
return env, env.init(false)
}
// DataDirBootstrapPath returns the path to the bootstrap file within the user's
// data dir. If the file does not exist there it will be found in the AppDirPath
// by ReloadBootstrap.
func (e *Env) DataDirBootstrapPath() string {
return filepath.Join(e.DataDirPath, "bootstrap.tgz")
}
// LoadBootstrap sets BootstrapPath to the given value, and loads BootstrapFS
// and all derived fields based on that.
func (e *Env) LoadBootstrap(path string) error {
var (
err error
// load all values into temp variables before setting the fields on Env,
// so we don't leave it in an inconsistent state.
bootstrapFS fs.FS
hosts map[string]Host
hostNameB []byte
)
if bootstrapFS, err = tarutil.FSFromTGZFile(path); err != nil {
return fmt.Errorf("reading bootstrap file at %q: %w", e.BootstrapPath, err)
}
if hosts, err = LoadHosts(bootstrapFS); err != nil {
return fmt.Errorf("loading hosts info from bootstrap fs: %w", err)
}
if hostNameB, err = fs.ReadFile(bootstrapFS, "hostname"); err != nil {
return fmt.Errorf("loading hostname from bootstrap fs: %w", err)
}
e.BootstrapPath = path
e.BootstrapFS = bootstrapFS
e.Hosts = hosts
e.HostName = string(hostNameB)
return nil
}
func (e *Env) initBootstrap(bootstrapOptional bool) error {
exists := func(path string) (bool, error) {
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat'ing %q: %w", path, err)
}
return true, nil
}
// start by checking if a bootstrap can be found in the user's data
// directory. This will only not be the case if daemon has never been
// successfully started.
{
bootstrapPath := e.DataDirBootstrapPath()
if exists, err := exists(bootstrapPath); err != nil {
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
} else if exists {
return e.LoadBootstrap(bootstrapPath)
}
}
// fallback to checking within the AppDir for a bootstrap which has been
// embedded into the binary.
{
bootstrapPath := filepath.Join(e.AppDirPath, "share/bootstrap.tgz")
if exists, err := exists(bootstrapPath); err != nil {
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
} else if !exists && !bootstrapOptional {
return fmt.Errorf("boostrap file not found at %q", bootstrapPath)
} else if exists {
return e.LoadBootstrap(bootstrapPath)
}
}
return nil
}
func (e *Env) init(bootstrapOptional bool) error {
var cancel context.CancelFunc
e.Context, cancel = context.WithCancel(context.Background())
signalCh := make(chan os.Signal, 2)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signalCh
cancel()
fmt.Fprintf(os.Stderr, "got signal %v, will exit gracefully\n", sig)
sig = <-signalCh
fmt.Fprintf(os.Stderr, "second interrupt signal %v received, force quitting, there may be zombie children left behind, good luck!\n", sig)
os.Stderr.Sync()
os.Exit(1)
}()
if err := e.initBootstrap(bootstrapOptional); err != nil {
return fmt.Errorf("initializing bootstrap data: %w", err)
}
return nil
}
// ThisHost is a shortcut for returning env.Hosts[env.HostName].
func (e *Env) ThisHost() Host {
return e.Hosts[e.HostName]
}
// ToMap returns the Env as a map of key/value strings. If this map is set into
// a process's environment, then that process can read it back using ReadEnv.
func (e *Env) ToMap() map[string]string {
return map[string]string{
DaemonYmlPathEnvVar: e.DaemonYmlPath,
BootstrapPathEnvVar: e.BootstrapPath,
RuntimeDirPathEnvVar: e.RuntimeDirPath,
DataDirPathEnvVar: e.DataDirPath,
}
}
// ThisDaemon returns the DaemonYml (loaded from DaemonYmlPath) for the
// currently running process.
func (e *Env) ThisDaemon() DaemonYml {
e.thisDaemonOnce.Do(func() {
if err := yamlutil.LoadYamlFile(&e.thisDaemon, e.DaemonYmlPath); err != nil {
panic(err)
}
})
return e.thisDaemon
}
// BinPath returns the absolute path to a binary in the AppDir.
func (e *Env) BinPath(name string) string {
return filepath.Join(e.AppDirPath, "bin", name)
}