package emulator import ( "context" "errors" "fmt" "os" "os/exec" "path" "path/filepath" "time" "github.com/go-logr/logr" "edge-infra.dev/pkg/lib/fog" "edge-infra.dev/pkg/sds/emergencyaccess/msgdata" "edge-infra.dev/pkg/sds/lib/colors" ) // This file holds the public methods (initialiser and run) for the emulator as well as fields // common to both the session and connect prompts // The emulator is responsible for: // - handling user input at the command line // - displaying output from connected sessions and errors in a user friendly manner // - storing history and workspace for ease of use // The connect and session prompts are functional elements that are built using go-prompt and // gleisonmv/colors const ( historyFileLimit = 5000 sessionTimeoutDefault = "300s" connectHistoryFileName = "rcli_connect_history" sessionHistoryFileName = "rcli_session_history" shellHistoryFileName = "rcli_shell_history" workspaceFileName = "rcli_workspace.json" UNSET = "UNSET" ) var ( ErrorRCLINoSubcmd = errors.New("no subcommands") ErrorRCLIUnknownSubcmd = errors.New("unrecognised subcommand") // Array of all shared exit commands used in both connect and session prompt exitCommands = []string{"q", "end", "exit"} ) type CLIService interface { Connect(ctx context.Context, projectID string, bannerID string, storeID string, terminalID string) error Send(command string) (commandID string, err error) SetTopicTemplate(topicTemplate string) SetSubscriptionTemplate(subscriptionTemplate string) RetrieveIdentity(context.Context) error GetDisplayChannel() <-chan msgdata.CommandResponse GetSessionContext() context.Context UserID() string IdleTime() time.Duration EnablePerSessionSubscription() DisablePerSessionSubscription() End() error // Env should return a list of environment variables, in the form name=value, // that can be used by the CLIService to reconnect to the same environment. // This is useful in local shell mode Env() []string } type DarkModeService interface { SetDarkmode(bool) Darkmode() bool } // go-prompt will break the terminal (no characters printed) if this isnt defered on exit func handleExit() { rawModeOff := exec.Command("/bin/stty", "-raw", "echo") rawModeOff.Stdin = os.Stdin _ = rawModeOff.Run() } func isPhase1() bool { // There's probably a much better way of doing all of this return path.Base(os.Args[0]) == "remotecliv1" } // Emulator struct to hold methods and private variables. // Only public methods should be the initialiser and Run as it is a ui driven library. type Emulator struct { cls CLIService runCtx context.Context //passed from run to connect executor log logr.Logger // Path to the directory which contains command history and workspace files. // Current working directory by default. config Config sessionHistory commandHistory connectHistory commandHistory shellHistory commandHistory workspace *workspace sessionPromptPaused bool commandOptions map[string]CommandOpts // Used to store the connection data for local shell mode connectData connectionData } // Takes the parent context from main and initialises cliservice. func NewEmulator(ctx context.Context, cls CLIService, config Config) Emulator { return Emulator{ cls: cls, log: fog.FromContext(ctx).WithName("emulator"), config: config, commandOptions: map[string]CommandOpts{}, } } func (em *Emulator) sessionPath() string { return filepath.Join(em.config.dir, sessionHistoryFileName) } func (em *Emulator) connectPath() string { return filepath.Join(em.config.dir, connectHistoryFileName) } func (em *Emulator) shellHistPath() string { return filepath.Join(em.config.dir, shellHistoryFileName) } func (em *Emulator) workspacePath() string { return filepath.Join(em.config.dir, workspaceFileName) } // Run starts the prompts and is blocking. Note that if the session prompt is active // the display channel will be buffered to and not printed. func (em *Emulator) Run(ctx context.Context) { defer handleExit() em.runCtx = ctx // Get user details err := em.cls.RetrieveIdentity(ctx) if err != nil { em.log.Error(err, "Failed to retrieve valid BSL token") fmt.Println(colors.Text("Error authenticating user. See logs for more detail.", colors.BgRed)) return } // Load workspace and command history files em.loadWorkspace(em.connectPath(), em.sessionPath(), em.workspacePath()) em.setupCompleters() if em.workspace.DisablePerSessionSubscription { em.cls.DisablePerSessionSubscription() } em.displayConnectBanner() em.connectPrompt(em.connectHistory.history).Run() err = em.workspace.save(em.workspacePath()) if err != nil { em.log.Error(err, "Error saving workspace") } } func (em *Emulator) loadWorkspace(connectFile, sessionFile, workspaceFile string) { var err error em.connectHistory, err = newCommandHistory(connectFile) if err != nil { em.log.Error(err, "Error establishing remote cli history") } em.sessionHistory, err = newCommandHistory(sessionFile) if err != nil { em.log.Error(err, "Error establishing session prompt history") } em.shellHistory, err = newCommandHistory(em.shellHistPath()) if err != nil { em.log.Error(err, "Error establishing shell prompt history") } em.workspace, err = newWorkspace(workspaceFile) if err != nil { em.log.Error(err, "Error establishing workspace") } } func getDarkmode(cls CLIService) bool { if dm, ok := cls.(DarkModeService); ok { return dm.Darkmode() } return false } // SetupCompleters is a method which sets up the appropriate completers for a // given phase binary func (em *Emulator) setupCompleters() { if isPhase1() { rcliconfigCompleter = append(rcliconfigCompleter, rcliConfigCompleterPhase1...) } else { topLevelConnectCommands = append(topLevelConnectCommands, connectCommandsCompleterPhase2...) } }