package crypticnet import ( "context" "cryptic-net/bootstrap" "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 Bootstrap bootstrap.Bootstrap 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 if e.Bootstrap, err = bootstrap.FromFile(path); err != nil { return fmt.Errorf("parsing bootstrap.tgz at %q: %w", path, err) } e.BootstrapPath = path 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 } // 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) }