-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement "runme beta session" #683
base: main
Are you sure you want to change the base?
Changes from all commits
70e89ef
031a759
f898779
7b31104
59de037
575afc9
575b31a
2f89508
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
package beta | ||
|
||
import ( | ||
"context" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"strconv" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/spf13/cobra" | ||
"go.uber.org/multierr" | ||
"go.uber.org/zap" | ||
|
||
"github.com/stateful/runme/v3/internal/command" | ||
"github.com/stateful/runme/v3/internal/config/autoconfig" | ||
runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2" | ||
) | ||
|
||
func sessionCmd(*commonFlags) *cobra.Command { | ||
cmd := cobra.Command{ | ||
Use: "session", | ||
Short: "Start shell within a session.", | ||
Long: `Start shell within a session. | ||
|
||
All exported variables during the session will be available to the subsequent commands. | ||
`, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
return autoconfig.InvokeForCommand( | ||
func( | ||
cmdFactory command.Factory, | ||
logger *zap.Logger, | ||
) error { | ||
defer logger.Sync() | ||
|
||
envs, err := executeDefaultShellProgram( | ||
cmd.Context(), | ||
cmdFactory, | ||
cmd.InOrStdin(), | ||
cmd.OutOrStdout(), | ||
cmd.ErrOrStderr(), | ||
nil, | ||
) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// TODO(adamb): currently, the collected env are printed out, | ||
// but they could be put in a session. | ||
if _, err := cmd.ErrOrStderr().Write([]byte("Collected env during the session:\n")); err != nil { | ||
return errors.WithStack(err) | ||
} | ||
|
||
for _, env := range envs { | ||
_, err := cmd.OutOrStdout().Write([]byte(env + "\n")) | ||
if err != nil { | ||
return errors.WithStack(err) | ||
} | ||
} | ||
|
||
return nil | ||
}, | ||
) | ||
}, | ||
} | ||
|
||
cmd.AddCommand(sessionSetupCmd()) | ||
|
||
return &cmd | ||
} | ||
|
||
func executeDefaultShellProgram( | ||
ctx context.Context, | ||
commandFactory command.Factory, | ||
stdin io.Reader, | ||
stdout io.Writer, | ||
stderr io.Writer, | ||
additionalEnv []string, | ||
) ([]string, error) { | ||
envCollector, err := command.NewEnvCollectorFactory().Build() | ||
if err != nil { | ||
return nil, errors.WithStack(err) | ||
} | ||
|
||
cfg := &command.ProgramConfig{ | ||
ProgramName: defaultShell(), | ||
Mode: runnerv2.CommandMode_COMMAND_MODE_CLI, | ||
Env: append( | ||
[]string{command.CreateEnv(command.EnvCollectorSessionEnvName, "1")}, | ||
append(envCollector.ExtraEnv(), additionalEnv...)..., | ||
), | ||
} | ||
options := command.CommandOptions{ | ||
NoShell: true, | ||
Stdin: stdin, | ||
Stdout: stdout, | ||
Stderr: stderr, | ||
} | ||
program, err := commandFactory.Build(cfg, options) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
err = program.Start(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
err = program.Wait(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
changed, _, err := envCollector.Diff() | ||
return changed, err | ||
} | ||
|
||
func defaultShell() string { | ||
shell := os.Getenv("SHELL") | ||
if shell == "" { | ||
shell, _ = exec.LookPath("bash") | ||
} | ||
if shell == "" { | ||
shell = "/bin/sh" | ||
} | ||
return shell | ||
} | ||
|
||
func sessionSetupCmd() *cobra.Command { | ||
var debug bool | ||
|
||
cmd := cobra.Command{ | ||
Use: "setup", | ||
Hidden: true, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
return autoconfig.InvokeForCommand( | ||
func( | ||
cmdFactory command.Factory, | ||
logger *zap.Logger, | ||
) error { | ||
defer logger.Sync() | ||
|
||
out := cmd.OutOrStdout() | ||
|
||
if err := requireEnvs( | ||
command.EnvCollectorSessionEnvName, | ||
command.EnvCollectorSessionPrePathEnvName, | ||
command.EnvCollectorSessionPostPathEnvName, | ||
); err != nil { | ||
logger.Info("session setup is skipped because the environment variable is not set", zap.Error(err)) | ||
return writeNoopShellCommand(out) | ||
} | ||
|
||
sessionSetupEnabled := os.Getenv(command.EnvCollectorSessionEnvName) | ||
if val, err := strconv.ParseBool(sessionSetupEnabled); err != nil || !val { | ||
logger.Debug("session setup is skipped", zap.Error(err), zap.Bool("value", val)) | ||
return writeNoopShellCommand(out) | ||
} | ||
Comment on lines
+154
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah, that's a good point. I didn't notice the clash. I will change it to something like |
||
|
||
envSetter := command.NewScriptEnvSetter( | ||
os.Getenv(command.EnvCollectorSessionPrePathEnvName), | ||
os.Getenv(command.EnvCollectorSessionPostPathEnvName), | ||
debug, | ||
) | ||
if err := envSetter.SetOnShell(out); err != nil { | ||
return err | ||
} | ||
|
||
if _, err := cmd.ErrOrStderr().Write([]byte("Runme session active. When you're done, execute \"exit\".\n")); err != nil { | ||
return errors.WithStack(err) | ||
} | ||
|
||
return nil | ||
}, | ||
) | ||
}, | ||
} | ||
|
||
cmd.Flags().BoolVar(&debug, "debug", false, "Enable debug mode.") | ||
|
||
return &cmd | ||
} | ||
|
||
func requireEnvs(names ...string) error { | ||
var err error | ||
for _, name := range names { | ||
if os.Getenv(name) == "" { | ||
err = multierr.Append(err, errors.Errorf("environment variable %q is required", name)) | ||
} | ||
} | ||
return err | ||
} | ||
|
||
func writeNoopShellCommand(w io.Writer) error { | ||
_, err := w.Write([]byte(":")) | ||
return errors.WithStack(err) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,6 +75,7 @@ func (c *terminalCommand) Wait(ctx context.Context) (err error) { | |
err = cErr | ||
} | ||
} | ||
|
||
return err | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing I don't get is how these won't ever produce an error? In other words how are they set in the first place if they are expected by
setup
@adambabik?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order is that
beta session
starts a shell process with those envs provided first. This shell process starts and executes, in the case of zsh, the.zshrc
file inside whichbeta session setup
is called.It's perfectly normal that it typically will error and output no-op shell command in
.zshrc
, but from the user point of view it should be invisible. We can recommend protectingsource <(/path/to/runme beta session setup)
with a conditional checkingcommand.EnvCollectorSessionEnvName
to avoid that.