container: remove global msg
All checks were successful
Test / Create distribution (push) Successful in 1m10s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m2s
Test / Flake checks (push) Successful in 1m47s
All checks were successful
Test / Create distribution (push) Successful in 1m10s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m2s
Test / Flake checks (push) Successful in 1m47s
This frees all container instances of side effects. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
@@ -17,17 +17,30 @@ import (
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/app"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func buildCommand(ctx context.Context, out io.Writer) command.Command {
|
||||
func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error {
|
||||
msg.SwapVerbose(flagVerbose)
|
||||
|
||||
if early.yamaLSM != nil {
|
||||
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if early.dumpable != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
return nil
|
||||
}).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||
|
||||
@@ -39,10 +52,10 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
|
||||
}
|
||||
|
||||
// config extraArgs...
|
||||
config := tryPath(args[0])
|
||||
config := tryPath(msg, args[0])
|
||||
config.Args = append(config.Args, args[1:]...)
|
||||
|
||||
app.Main(ctx, config)
|
||||
app.Main(ctx, msg, config)
|
||||
panic("unreachable")
|
||||
})
|
||||
|
||||
@@ -78,9 +91,9 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
|
||||
passwd *user.User
|
||||
passwdOnce sync.Once
|
||||
passwdFunc = func() {
|
||||
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity))
|
||||
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustIDMsg(msg), flagIdentity))
|
||||
if u, err := user.LookupId(us); err != nil {
|
||||
hlog.Verbosef("cannot look up uid %s", us)
|
||||
msg.Verbosef("cannot look up uid %s", us)
|
||||
passwd = &user.User{
|
||||
Uid: us,
|
||||
Gid: us,
|
||||
@@ -162,7 +175,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
|
||||
}
|
||||
}
|
||||
|
||||
app.Main(ctx, config)
|
||||
app.Main(ctx, msg, config)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
@@ -198,13 +211,13 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
|
||||
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||
switch len(args) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, flagShort, flagJSON)
|
||||
printShowSystem(msg, os.Stdout, flagShort, flagJSON)
|
||||
|
||||
case 1: // instance
|
||||
name := args[0]
|
||||
config, entry := tryShort(name)
|
||||
config, entry := tryShort(msg, name)
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
config = tryPath(msg, name)
|
||||
}
|
||||
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
|
||||
|
||||
@@ -219,8 +232,8 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
|
||||
var flagShort bool
|
||||
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||
var sc hst.Paths
|
||||
app.CopyPaths(&sc, new(app.Hsu).MustID())
|
||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON)
|
||||
app.CopyPaths(msg, &sc, new(app.Hsu).MustID())
|
||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(msg, sc.RunDirPath.String()), flagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
)
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
@@ -68,7 +69,7 @@ Flags:
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := new(bytes.Buffer)
|
||||
c := buildCommand(t.Context(), out)
|
||||
c := buildCommand(t.Context(), container.NewMsg(nil), new(earlyHardeningErrs), out)
|
||||
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
||||
t.Errorf("Parse: error = %v; want %v",
|
||||
err, command.ErrHelp)
|
||||
|
||||
@@ -13,8 +13,6 @@ import (
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,20 +22,20 @@ var (
|
||||
license string
|
||||
)
|
||||
|
||||
func init() { hlog.Prepare("hakurei") }
|
||||
// earlyHardeningErrs are errors collected while setting up early hardening feature.
|
||||
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
|
||||
|
||||
func main() {
|
||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||
container.TryArgv0(nil)
|
||||
|
||||
if err := container.SetPtracer(0); err != nil {
|
||||
hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
}
|
||||
log.SetPrefix("hakurei: ")
|
||||
log.SetFlags(0)
|
||||
msg := container.NewMsg(log.Default())
|
||||
|
||||
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
early := earlyHardeningErrs{
|
||||
yamaLSM: container.SetPtracer(0),
|
||||
dumpable: container.SetDumpable(container.SUID_DUMP_DISABLE),
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
@@ -48,10 +46,10 @@ func main() {
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
|
||||
buildCommand(ctx, os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
buildCommand(ctx, msg, &early, os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
msg.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
msg.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
// this catches faulty command handlers that fail to return before this point
|
||||
|
||||
@@ -10,20 +10,20 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
func tryPath(name string) (config *hst.Config) {
|
||||
func tryPath(msg container.Msg, name string) (config *hst.Config) {
|
||||
var r io.Reader
|
||||
config = new(hst.Config)
|
||||
|
||||
if name != "-" {
|
||||
r = tryFd(name)
|
||||
r = tryFd(msg, name)
|
||||
if r == nil {
|
||||
hlog.Verbose("load configuration from file")
|
||||
msg.Verbose("load configuration from file")
|
||||
|
||||
if f, err := os.Open(name); err != nil {
|
||||
log.Fatalf("cannot access configuration file %q: %s", name, err)
|
||||
@@ -49,14 +49,14 @@ func tryPath(name string) (config *hst.Config) {
|
||||
return
|
||||
}
|
||||
|
||||
func tryFd(name string) io.ReadCloser {
|
||||
func tryFd(msg container.Msg, name string) io.ReadCloser {
|
||||
if v, err := strconv.Atoi(name); err != nil {
|
||||
if !errors.Is(err, strconv.ErrSyntax) {
|
||||
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||
msg.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
hlog.Verbosef("trying config stream from %d", v)
|
||||
msg.Verbosef("trying config stream from %d", v)
|
||||
fd := uintptr(v)
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
|
||||
if errors.Is(errno, syscall.EBADF) {
|
||||
@@ -68,7 +68,7 @@ func tryFd(name string) io.ReadCloser {
|
||||
}
|
||||
}
|
||||
|
||||
func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||
func tryShort(msg container.Msg, name string) (config *hst.Config, entry *state.State) {
|
||||
likePrefix := false
|
||||
if len(name) <= 32 {
|
||||
likePrefix = true
|
||||
@@ -86,11 +86,11 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||
|
||||
// try to match from state store
|
||||
if likePrefix && len(name) >= 8 {
|
||||
hlog.Verbose("argument looks like prefix")
|
||||
msg.Verbose("argument looks like prefix")
|
||||
|
||||
var sc hst.Paths
|
||||
app.CopyPaths(&sc, new(app.Hsu).MustID())
|
||||
s := state.NewMulti(sc.RunDirPath.String())
|
||||
app.CopyPaths(msg, &sc, new(app.Hsu).MustID())
|
||||
s := state.NewMulti(msg, sc.RunDirPath.String())
|
||||
if entries, err := state.Join(s); err != nil {
|
||||
log.Printf("cannot join store: %v", err)
|
||||
// drop to fetch from file
|
||||
@@ -104,7 +104,7 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||
break
|
||||
}
|
||||
|
||||
hlog.Verbosef("instance %s skipped", v)
|
||||
msg.Verbosef("instance %s skipped", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,19 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
func printShowSystem(msg container.Msg, output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
info := &hst.Info{User: new(app.Hsu).MustID()}
|
||||
app.CopyPaths(&info.Paths, info.User)
|
||||
app.CopyPaths(msg, &info.Paths, info.User)
|
||||
|
||||
if flagJSON {
|
||||
printJSON(output, short, info)
|
||||
|
||||
@@ -13,22 +13,21 @@ import (
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
)
|
||||
|
||||
func init() {
|
||||
hlog.Prepare("hpkg")
|
||||
func main() {
|
||||
log.SetPrefix("hpkg: ")
|
||||
log.SetFlags(0)
|
||||
msg := container.NewMsg(log.Default())
|
||||
|
||||
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
|
||||
log.Fatalf("cannot set $SHELL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
@@ -41,7 +40,7 @@ func main() {
|
||||
flagVerbose bool
|
||||
flagDropShell bool
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { msg.SwapVerbose(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
|
||||
|
||||
@@ -90,12 +89,12 @@ func main() {
|
||||
}
|
||||
cleanup := func() {
|
||||
// should be faster than a native implementation
|
||||
mustRun(chmod, "-R", "+w", workDir.String())
|
||||
mustRun(rm, "-rf", workDir.String())
|
||||
mustRun(msg, chmod, "-R", "+w", workDir.String())
|
||||
mustRun(msg, rm, "-rf", workDir.String())
|
||||
}
|
||||
beforeRunFail.Store(&cleanup)
|
||||
|
||||
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
|
||||
mustRun(msg, tar, "-C", workDir.String(), "-xf", pkgPath)
|
||||
|
||||
/*
|
||||
Parse bundle and app metadata, do pre-install checks.
|
||||
@@ -148,10 +147,10 @@ func main() {
|
||||
}
|
||||
|
||||
// sec: should compare version string
|
||||
hlog.Verbosef("installing application %q version %q over local %q",
|
||||
msg.Verbosef("installing application %q version %q over local %q",
|
||||
bundle.ID, bundle.Version, a.Version)
|
||||
} else {
|
||||
hlog.Verbosef("application %q clean installation", bundle.ID)
|
||||
msg.Verbosef("application %q clean installation", bundle.ID)
|
||||
// sec: should install credentials
|
||||
}
|
||||
|
||||
@@ -159,7 +158,7 @@ func main() {
|
||||
Setup steps for files owned by the target user.
|
||||
*/
|
||||
|
||||
withCacheDir(ctx, "install", []string{
|
||||
withCacheDir(ctx, msg, "install", []string{
|
||||
// export inner bundle path in the environment
|
||||
"export BUNDLE=" + hst.Tmp + "/bundle",
|
||||
// replace inner /etc
|
||||
@@ -181,7 +180,7 @@ func main() {
|
||||
}, workDir, bundle, pathSet, flagDropShell, cleanup)
|
||||
|
||||
if bundle.GPU {
|
||||
withCacheDir(ctx, "mesa-wrappers", []string{
|
||||
withCacheDir(ctx, msg, "mesa-wrappers", []string{
|
||||
// link nixGL mesa wrappers
|
||||
"mkdir -p nix/.nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
|
||||
@@ -193,7 +192,7 @@ func main() {
|
||||
Activate home-manager generation.
|
||||
*/
|
||||
|
||||
withNixDaemon(ctx, "activate", []string{
|
||||
withNixDaemon(ctx, msg, "activate", []string{
|
||||
// clean up broken links
|
||||
"mkdir -p .local/state/{nix,home-manager}",
|
||||
"chmod -R +w .local/state/{nix,home-manager}",
|
||||
@@ -261,7 +260,7 @@ func main() {
|
||||
*/
|
||||
|
||||
if a.GPU && flagAutoDrivers {
|
||||
withNixDaemon(ctx, "nix-gl", []string{
|
||||
withNixDaemon(ctx, msg, "nix-gl", []string{
|
||||
"mkdir -p /nix/.nixGL/auto",
|
||||
"rm -rf /nix/.nixGL/auto",
|
||||
"export NIXPKGS_ALLOW_UNFREE=1",
|
||||
@@ -316,7 +315,7 @@ func main() {
|
||||
Spawn app.
|
||||
*/
|
||||
|
||||
mustRunApp(ctx, config, func() {})
|
||||
mustRunApp(ctx, msg, config, func() {})
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
|
||||
@@ -324,9 +323,9 @@ func main() {
|
||||
}
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
msg.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
msg.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
const bash = "bash"
|
||||
@@ -51,8 +50,8 @@ func lookPath(file string) string {
|
||||
|
||||
var beforeRunFail = new(atomic.Pointer[func()])
|
||||
|
||||
func mustRun(name string, arg ...string) {
|
||||
hlog.Verbosef("spawning process: %q %q", name, arg)
|
||||
func mustRun(msg container.Msg, name string, arg ...string) {
|
||||
msg.Verbosef("spawning process: %q %q", name, arg)
|
||||
cmd := exec.Command(name, arg...)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var hakureiPath = internal.MustHakureiPath()
|
||||
|
||||
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
|
||||
func mustRunApp(ctx context.Context, msg container.Msg, config *hst.Config, beforeFail func()) {
|
||||
var (
|
||||
cmd *exec.Cmd
|
||||
st io.WriteCloser
|
||||
@@ -26,10 +26,10 @@ func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot pipe: %v", err)
|
||||
} else {
|
||||
if hlog.Load() {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
|
||||
if msg.IsVerbose() {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath.String(), "-v", "app", "3")
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
|
||||
cmd = exec.CommandContext(ctx, hakureiPath.String(), "app", "3")
|
||||
}
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.ExtraFiles = []*os.File{r}
|
||||
@@ -51,7 +51,8 @@ func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
beforeFail()
|
||||
internal.Exit(exitError.ExitCode())
|
||||
msg.BeforeExit()
|
||||
os.Exit(exitError.ExitCode())
|
||||
} else {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot wait: %v", err)
|
||||
|
||||
@@ -2,20 +2,21 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
)
|
||||
|
||||
func withNixDaemon(
|
||||
ctx context.Context,
|
||||
msg container.Msg,
|
||||
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
|
||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
||||
) {
|
||||
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
|
||||
mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: pathShell,
|
||||
@@ -61,9 +62,10 @@ func withNixDaemon(
|
||||
|
||||
func withCacheDir(
|
||||
ctx context.Context,
|
||||
msg container.Msg,
|
||||
action string, command []string, workDir *container.Absolute,
|
||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||
mustRunAppDropShell(ctx, &hst.Config{
|
||||
mustRunAppDropShell(ctx, msg, &hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: pathShell,
|
||||
@@ -97,12 +99,13 @@ func withCacheDir(
|
||||
}, dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
|
||||
func mustRunAppDropShell(ctx context.Context, msg container.Msg, config *hst.Config, dropShell bool, beforeFail func()) {
|
||||
if dropShell {
|
||||
config.Args = []string{bash, "-l"}
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
mustRunApp(ctx, msg, config, beforeFail)
|
||||
beforeFail()
|
||||
internal.Exit(0)
|
||||
msg.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
mustRunApp(ctx, msg, config, beforeFail)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user