implement /reset:cwd (fix for #278) and more (#392)

* checkpoint some ideas on a new branch

* checkpoint on new errors / errorcode passing

* get CodedError piped all the way through to infomsg

* implement a /reset:cwd command to deal with cases when the cwd is invalid.  other assorted debugging, utility, and fixups

* on invalid cwd, show message to run /reset:cwd
This commit is contained in:
Mike Sawka 2024-03-06 16:37:54 -08:00 committed by GitHub
parent 33fc3518c0
commit 4357bcf1c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 301 additions and 40 deletions

View File

@ -61,3 +61,6 @@ export enum StatusIndicatorLevel {
Success = 2,
Error = 3,
}
// matches packet.go
export const ErrorCode_InvalidCwd = "ERRCWD";

View File

@ -407,7 +407,7 @@
}
.info-msg {
color: var(--cmdinput-history-title-color);
color: var(--term-blue);
padding-bottom: 2px;
a {

View File

@ -8,7 +8,7 @@ import cn from "classnames";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models";
import { makeExternLink } from "@/util/util";
import * as appconst from "@/app/appconst";
dayjs.extend(localizedFormat);
@ -104,6 +104,9 @@ class InfoMsg extends React.Component<{}, {}> {
<div key="infoerror" className="info-error">
[error] {infoMsg.infoerror}
</div>
<If condition={infoMsg.infoerrorcode == appconst.ErrorCode_InvalidCwd}>
<div className="info-error">to reset, run: /reset:cwd</div>
</If>
</If>
</div>
);

View File

@ -1444,7 +1444,11 @@ class Model {
if (err?.message) {
errMsg = err.message;
}
this.inputModel.flashInfoMsg({ infoerror: errMsg }, null);
let info: InfoType = { infoerror: errMsg };
if (err?.errorcode) {
info.infoerrorcode = err.errorcode;
}
this.inputModel.flashInfoMsg(info, null);
}
}

View File

@ -419,6 +419,7 @@ declare global {
infomsghtml?: boolean;
websharelink?: boolean;
infoerror?: string;
infoerrorcode?: string;
infolines?: string[];
infocomps?: string[];
infocompsmore?: boolean;

View File

@ -50,7 +50,11 @@ function fetchJsonData(resp: any, ctErr: boolean): Promise<any> {
throw rtnErr;
}
if (rtnData?.error) {
throw new Error(rtnData.error);
let err = new Error(rtnData.error);
if (rtnData.errorcode) {
err["errorcode"] = rtnData.errorcode;
}
throw err;
}
return rtnData;
});

View File

@ -0,0 +1,37 @@
package base
import "fmt"
type CodedError struct {
ErrorCode string
Err error
}
func (c *CodedError) Error() string {
return fmt.Sprintf("%s %s", c.ErrorCode, c.Err.Error())
}
func (c *CodedError) Unwrap() error {
return c.Err
}
func MakeCodedError(code string, err error) *CodedError {
return &CodedError{
ErrorCode: code,
Err: err,
}
}
func CodedErrorf(code string, format string, args ...interface{}) *CodedError {
return &CodedError{
ErrorCode: code,
Err: fmt.Errorf(format, args...),
}
}
func GetErrorCode(err error) string {
if codedErr, ok := err.(*CodedError); ok {
return codedErr.ErrorCode
}
return ""
}

View File

@ -72,6 +72,10 @@ const (
ShellType_zsh = "zsh"
)
const (
EC_InvalidCwd = "ERRCWD"
)
const PacketSenderQueueSize = 20
const PacketEOFStr = "EOF"
@ -491,11 +495,12 @@ func MakeCompGenPacket() *CompGenPacketType {
}
type ResponsePacketType struct {
Type string `json:"type"`
RespId string `json:"respid"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
Data interface{} `json:"data,omitempty"`
Type string `json:"type"`
RespId string `json:"respid"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
ErrorCode string `json:"errorcode,omitempty"` // can be used for structured errors
Data interface{} `json:"data,omitempty"`
}
func (*ResponsePacketType) GetType() string {
@ -516,6 +521,9 @@ func (p *ResponsePacketType) Err() error {
}
if !p.Success {
if p.Error != "" {
if p.ErrorCode != "" {
return &base.CodedError{ErrorCode: p.ErrorCode, Err: errors.New(p.Error)}
}
return errors.New(p.Error)
}
return fmt.Errorf("rpc failed")
@ -531,6 +539,9 @@ func (p *ResponsePacketType) String() string {
}
func MakeErrorResponsePacket(reqId string, err error) *ResponsePacketType {
if codedErr, ok := err.(*base.CodedError); ok {
return &ResponsePacketType{Type: ResponsePacketStr, RespId: reqId, Error: codedErr.Err.Error(), ErrorCode: codedErr.ErrorCode}
}
return &ResponsePacketType{Type: ResponsePacketStr, RespId: reqId, Error: err.Error()}
}

View File

@ -231,10 +231,25 @@ func (sdiff *ShellStateDiff) GetHashVal(force bool) string {
return sdiff.HashVal
}
func (state ShellState) Dump() {
fmt.Printf("ShellState:\n")
fmt.Printf(" version: %s\n", state.Version)
fmt.Printf(" shelltype: %s\n", state.GetShellType())
fmt.Printf(" hashval: %s\n", state.GetHashVal(false))
fmt.Printf(" cwd: %s\n", state.Cwd)
fmt.Printf(" vars: %d, aliases: %d, funcs: %d\n", len(state.ShellVars), len(state.Aliases), len(state.Funcs))
if state.Error != "" {
fmt.Printf(" error: %s\n", state.Error)
}
}
func (sdiff ShellStateDiff) Dump(vars bool, aliases bool, funcs bool) {
fmt.Printf("ShellStateDiff:\n")
fmt.Printf(" version: %s\n", sdiff.Version)
fmt.Printf(" base: %s\n", sdiff.BaseHash)
fmt.Printf(" diffhash: %s\n", sdiff.GetHashVal(false))
fmt.Printf(" diffhasharr: %v\n", sdiff.DiffHashArr)
fmt.Printf(" cwd: %s\n", sdiff.Cwd)
fmt.Printf(" vars: %d, aliases: %d, funcs: %d\n", len(sdiff.VarsDiff), len(sdiff.AliasesDiff), len(sdiff.FuncsDiff))
if sdiff.Error != "" {
fmt.Printf(" error: %s\n", sdiff.Error)

View File

@ -79,8 +79,22 @@ func (b bashShellApi) MakeShExecCommand(cmdStr string, rcFileName string, usePty
return MakeBashShExecCommand(cmdStr, rcFileName, usePty)
}
func (b bashShellApi) GetShellState() (*packet.ShellState, error) {
return GetBashShellState()
func (b bashShellApi) GetShellState() chan ShellStateOutput {
ch := make(chan ShellStateOutput, 1)
defer close(ch)
ssPk, err := GetBashShellState()
if err != nil {
ch <- ShellStateOutput{
Status: ShellStateOutputStatus_Done,
Error: err.Error(),
}
return ch
}
ch <- ShellStateOutput{
Status: ShellStateOutputStatus_Done,
ShellState: ssPk,
}
return ch
}
func (b bashShellApi) GetBaseShellOpts() string {

View File

@ -48,6 +48,17 @@ type RunCommandOpts struct {
CommandStdinFdNum int // needed for SudoWithPass
}
const (
ShellStateOutputStatus_Done = "done"
)
type ShellStateOutput struct {
Status string
StderrOutput []byte
ShellState *packet.ShellState
Error string
}
type ShellApi interface {
GetShellType() string
MakeExitTrap(fdNum int) string
@ -56,7 +67,7 @@ type ShellApi interface {
GetRemoteShellPath() string
MakeRunCommand(cmdStr string, opts RunCommandOpts) string
MakeShExecCommand(cmdStr string, rcFileName string, usePty bool) *exec.Cmd
GetShellState() (*packet.ShellState, error)
GetShellState() chan ShellStateOutput
GetBaseShellOpts() string
ParseShellStateOutput(output []byte) (*packet.ShellState, error)
MakeRcFileStr(pk *packet.RunPacketType) string
@ -64,6 +75,9 @@ type ShellApi interface {
ApplyShellStateDiff(oldState *packet.ShellState, diff *packet.ShellStateDiff) (*packet.ShellState, error)
}
var _ ShellApi = &bashShellApi{}
var _ ShellApi = &zshShellApi{}
func DetectLocalShellType() string {
shellPath := GetMacUserShell()
if shellPath == "" {

View File

@ -203,20 +203,25 @@ func (z zshShellApi) MakeShExecCommand(cmdStr string, rcFileName string, usePty
return exec.Command(GetLocalZshPath(), "-l", "-i", "-c", cmdStr)
}
func (z zshShellApi) GetShellState() (*packet.ShellState, error) {
func (z zshShellApi) GetShellState() chan ShellStateOutput {
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
defer cancelFn()
rtnCh := make(chan ShellStateOutput, 1)
defer close(rtnCh)
cmdStr := BaseZshOpts + "; " + GetZshShellStateCmd(StateOutputFdNum)
ecmd := exec.CommandContext(ctx, GetLocalZshPath(), "-l", "-i", "-c", cmdStr)
_, outputBytes, err := RunCommandWithExtraFd(ecmd, StateOutputFdNum)
if err != nil {
return nil, err
rtnCh <- ShellStateOutput{Status: ShellStateOutputStatus_Done, Error: err.Error()}
return rtnCh
}
rtn, err := z.ParseShellStateOutput(outputBytes)
if err != nil {
return nil, err
rtnCh <- ShellStateOutput{Status: ShellStateOutputStatus_Done, Error: err.Error()}
return rtnCh
}
return rtn, nil
rtnCh <- ShellStateOutput{Status: ShellStateOutputStatus_Done, ShellState: rtn}
return rtnCh
}
func (z zshShellApi) GetBaseShellOpts() string {

View File

@ -7,6 +7,7 @@ import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
@ -368,14 +369,18 @@ func ValidateRunPacket(pk *packet.RunPacketType) error {
return fmt.Errorf("cannot detach command, constant rundata input too large len=%d, max=%d", totalRunData, mpio.MaxTotalRunDataSize)
}
}
if pk.State != nil && pk.State.Cwd != "" {
realCwd := base.ExpandHomeDir(pk.State.Cwd)
if pk.State != nil {
pkCwd := pk.State.Cwd
if pkCwd == "" {
pkCwd = "~"
}
realCwd := base.ExpandHomeDir(pkCwd)
dirInfo, err := os.Stat(realCwd)
if err != nil {
return fmt.Errorf("invalid cwd '%s' for command: %v", realCwd, err)
return base.CodedErrorf(packet.EC_InvalidCwd, "invalid cwd '%s' for command: %v", realCwd, err)
}
if !dirInfo.IsDir() {
return fmt.Errorf("invalid cwd '%s' for command, not a directory", realCwd)
return base.CodedErrorf(packet.EC_InvalidCwd, "invalid cwd '%s' for command, not a directory", realCwd)
}
}
for _, runData := range pk.RunData {
@ -896,7 +901,9 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
if sapi.GetShellType() == packet.ShellType_zsh {
shellutil.UpdateCmdEnv(cmd.Cmd, map[string]string{"ZDOTDIR": zdotdir})
}
if state.Cwd != "" {
if state.Cwd == "" {
cmd.Cmd.Dir = base.ExpandHomeDir("~")
} else if state.Cwd != "" {
cmd.Cmd.Dir = base.ExpandHomeDir(state.Cwd)
}
err = ValidateRemoteFds(pk.Fds)
@ -1237,12 +1244,13 @@ func MakeShellStatePacket(shellType string) (*packet.ShellStatePacketType, error
if err != nil {
return nil, err
}
shellState, err := sapi.GetShellState()
if err != nil {
return nil, err
rtnCh := sapi.GetShellState()
ssOutput := <-rtnCh
if ssOutput.Error != "" {
return nil, errors.New(ssOutput.Error)
}
rtn := packet.MakeShellStatePacket()
rtn.State = shellState
rtn.State = ssOutput.ShellState
return rtn, nil
}

View File

@ -623,6 +623,10 @@ func WriteJsonError(w http.ResponseWriter, errVal error) {
w.WriteHeader(200)
errMap := make(map[string]interface{})
errMap["error"] = errVal.Error()
errorCode := base.GetErrorCode(errVal)
if errorCode != "" {
errMap["errorcode"] = errorCode
}
barr, _ := json.Marshal(errMap)
w.Write(barr)
}

View File

@ -167,6 +167,7 @@ func init() {
registerCmdFn("_compgen", CompGenCommand)
registerCmdFn("clear", ClearCommand)
registerCmdFn("reset", RemoteResetCommand)
registerCmdFn("reset:cwd", ResetCwdCommand)
registerCmdFn("signal", SignalCommand)
registerCmdFn("sync", SyncCommand)
@ -190,6 +191,7 @@ func init() {
registerCmdFn("screen:reset", ScreenResetCommand)
registerCmdFn("screen:webshare", ScreenWebShareCommand)
registerCmdFn("screen:reorder", ScreenReorderCommand)
registerCmdFn("screen:show", ScreenShowCommand)
registerCmdAlias("remote", RemoteCommand)
registerCmdFn("remote:show", RemoteShowCommand)
@ -707,7 +709,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.U
if rtnErr == nil {
update, rtnErr = HandleCommand(ctxWithHistory, newPk)
} else {
return nil, fmt.Errorf("error in Eval Meta Command: %v", rtnErr)
return nil, fmt.Errorf("error in Eval Meta Command: %w", rtnErr)
}
if !resolveBool(pk.Kwargs["nohist"], false) {
// TODO should this be "pk" or "newPk" (2nd arg)
@ -3419,6 +3421,40 @@ func SessionArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType
}
}
func ScreenShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
if err != nil {
return nil, err
}
screen, err := sstore.GetScreenById(ctx, ids.ScreenId)
if err != nil {
return nil, fmt.Errorf("cannot get screen: %v", err)
}
if screen == nil {
return nil, fmt.Errorf("screen not found")
}
statePtr, err := remote.ResolveCurrentScreenStatePtr(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr)
if err != nil {
return nil, fmt.Errorf("cannot resolve current screen stateptr: %v", err)
}
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "screenid", screen.ScreenId))
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "name", screen.Name))
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "screenidx", screen.ScreenIdx))
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "tabcolor", screen.ScreenOpts.TabColor))
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "tabicon", screen.ScreenOpts.TabIcon))
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "selectedline", screen.SelectedLine))
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "curremote", GetFullRemoteDisplayName(&screen.CurRemote, &ids.Remote.RState)))
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "stateptr-base", statePtr.BaseHash))
buf.WriteString(fmt.Sprintf(" %-15s %v\n", "stateptr-diff", statePtr.DiffHashArr))
update := scbus.MakeUpdatePacket()
update.AddUpdate(sstore.InfoMsgType{
InfoTitle: "screen info",
InfoLines: splitLinesForInfo(buf.String()),
})
return update, nil
}
func SessionShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
ids, err := resolveUiIds(ctx, pk, R_Session)
if err != nil {
@ -3600,6 +3636,33 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
return update, nil
}
func ResetCwdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
if err != nil {
return nil, err
}
statePtr, err := remote.ResolveCurrentScreenStatePtr(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr)
if err != nil {
return nil, err
}
stateDiff, err := sstore.GetCurStateDiffFromPtr(ctx, statePtr)
if err != nil {
return nil, err
}
feState := ids.Remote.FeState
feState["cwd"] = "~"
stateDiff.Cwd = "~"
stateDiff.GetHashVal(true)
remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, feState, nil, stateDiff)
if err != nil {
return nil, fmt.Errorf("could not update remote state: %v", err)
}
update := scbus.MakeUpdatePacket()
update.AddUpdate(sstore.MakeSessionUpdateForRemote(ids.SessionId, remoteInst), sstore.InteractiveUpdate(pk.Interactive))
update.AddUpdate(sstore.InfoMsgType{InfoMsg: "reset cwd to ~"})
return update, nil
}
func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
if err != nil {
@ -3958,13 +4021,13 @@ func LineRestartCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
NoCreateCmdPtyFile: true,
}
cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket)
sstore.IncrementNumRunningCmds(cmd.ScreenId, 1)
if callback != nil {
defer callback()
}
if err != nil {
return nil, err
}
sstore.IncrementNumRunningCmds(cmd.ScreenId, 1)
newTs := time.Now().UnixMilli()
err = sstore.UpdateCmdForRestart(ctx, runPacket.CK, newTs, cmd.CmdPid, cmd.RemotePid, convertTermOpts(runPacket.TermOpts))
if err != nil {

View File

@ -1919,6 +1919,25 @@ func (msh *MShellProc) removePendingStateCmd(screenId string, rptr sstore.Remote
}
}
func ResolveCurrentScreenStatePtr(ctx context.Context, sessionId string, screenId string, remotePtr sstore.RemotePtrType) (*sstore.ShellStatePtr, error) {
statePtr, err := sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr)
if err != nil {
return nil, fmt.Errorf("cannot get current connection stateptr: %w", err)
}
if statePtr == nil {
msh := GetRemoteById(remotePtr.RemoteId)
err := msh.EnsureShellType(ctx, msh.GetShellPref()) // make sure shellType is initialized
if err != nil {
return nil, err
}
statePtr = msh.GetDefaultStatePtr(msh.GetShellPref())
if statePtr == nil {
return nil, fmt.Errorf("no valid default connection stateptr")
}
}
return statePtr, nil
}
type RunCommandOpts struct {
SessionId string
ScreenId string
@ -1993,19 +2012,9 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
statePtr = rcOpts.StatePtr
} else {
var err error
statePtr, err = sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr)
statePtr, err = ResolveCurrentScreenStatePtr(ctx, sessionId, screenId, remotePtr)
if err != nil {
return nil, nil, fmt.Errorf("cannot get current connection stateptr: %w", err)
}
}
if statePtr == nil { // can be null if there is no remote-instance (screen has unchanged state from default)
err := msh.EnsureShellType(ctx, msh.GetShellPref()) // make sure shellType is initialized
if err != nil {
return nil, nil, err
}
statePtr = msh.GetDefaultStatePtr(msh.GetShellPref())
if statePtr == nil {
return nil, nil, fmt.Errorf("cannot run command, no valid connection stateptr")
return nil, nil, fmt.Errorf("cannot run command: %w", err)
}
}
currentState, err := sstore.GetFullState(ctx, *statePtr)
@ -2049,7 +2058,7 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
return nil, nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk))
}
if respPk.Error != "" {
return nil, nil, errors.New(respPk.Error)
return nil, nil, respPk.Err()
}
return nil, nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk))
}

View File

@ -2023,6 +2023,71 @@ func StoreStateDiff(ctx context.Context, diff *packet.ShellStateDiff) error {
return nil
}
func GetStateBaseVersion(ctx context.Context, baseHash string) (string, error) {
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
query := `SELECT version FROM state_base WHERE basehash = ?`
rtn := tx.GetString(query, baseHash)
return rtn, nil
})
}
func GetCurStateDiffFromPtr(ctx context.Context, ssPtr *ShellStatePtr) (*packet.ShellStateDiff, error) {
if ssPtr == nil {
return nil, fmt.Errorf("cannot resolve state, empty stateptr")
}
if len(ssPtr.DiffHashArr) == 0 {
baseVersion, err := GetStateBaseVersion(ctx, ssPtr.BaseHash)
if err != nil {
return nil, fmt.Errorf("cannot get base version: %v", err)
}
// return an empty diff
return &packet.ShellStateDiff{Version: baseVersion, BaseHash: ssPtr.BaseHash}, nil
}
lastDiffHash := ssPtr.DiffHashArr[len(ssPtr.DiffHashArr)-1]
return GetStateDiff(ctx, lastDiffHash)
}
func GetStateBase(ctx context.Context, baseHash string) (*packet.ShellState, error) {
stateBase, txErr := WithTxRtn(ctx, func(tx *TxWrap) (*StateBase, error) {
var stateBase StateBase
query := `SELECT * FROM state_base WHERE basehash = ?`
found := tx.Get(&stateBase, query, baseHash)
if !found {
return nil, fmt.Errorf("StateBase %s not found", baseHash)
}
return &stateBase, nil
})
if txErr != nil {
return nil, txErr
}
state := &packet.ShellState{}
err := state.DecodeShellState(stateBase.Data)
if err != nil {
return nil, err
}
return state, nil
}
func GetStateDiff(ctx context.Context, diffHash string) (*packet.ShellStateDiff, error) {
stateDiff, txErr := WithTxRtn(ctx, func(tx *TxWrap) (*StateDiff, error) {
query := `SELECT * FROM state_diff WHERE diffhash = ?`
stateDiff := dbutil.GetMapGen[*StateDiff](tx, query, diffHash)
if stateDiff == nil {
return nil, fmt.Errorf("StateDiff %s not found", diffHash)
}
return stateDiff, nil
})
if txErr != nil {
return nil, txErr
}
state := &packet.ShellStateDiff{}
err := state.DecodeShellStateDiff(stateDiff.Data)
if err != nil {
return nil, err
}
return state, nil
}
// returns error when not found
func GetFullState(ctx context.Context, ssPtr ShellStatePtr) (*packet.ShellState, error) {
var state *packet.ShellState

View File

@ -48,6 +48,7 @@ func (CmdLineUpdate) GetType() string {
type InfoMsgType struct {
InfoTitle string `json:"infotitle"`
InfoError string `json:"infoerror,omitempty"`
InfoErrorCode string `json:"infoerrorcode,omitempty"`
InfoMsg string `json:"infomsg,omitempty"`
InfoMsgHtml bool `json:"infomsghtml,omitempty"`
WebShareLink bool `json:"websharelink,omitempty"`