mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-05 19:08:59 +01:00
b733724c7d
* Server impl * add update check setting * add commands * fix capitalization of commands * apply suggestions * add migration and fix backend bugs * Add sidebar banner * remove installedversion, add 5s timeout * add icon, capture and log errors from release check * missing return nil * remove highlight * remove commented less * do not fail releasecheckoncommand if release check operation fails * remove debug condition * fix update on auto check, move banner display logic into frontend * remove unnecessary import * simplify null check * clean up the invoking of the releasechecker
4022 lines
126 KiB
Go
4022 lines
126 KiB
Go
// Copyright 2023, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package cmdrunner
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/comp"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote/openai"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
const (
|
|
HistoryTypeScreen = "screen"
|
|
HistoryTypeSession = "session"
|
|
HistoryTypeGlobal = "global"
|
|
)
|
|
|
|
func init() {
|
|
comp.RegisterSimpleCompFn(comp.CGTypeMeta, simpleCompMeta)
|
|
comp.RegisterSimpleCompFn(comp.CGTypeCommandMeta, simpleCompCommandMeta)
|
|
}
|
|
|
|
const DefaultUserId = "user"
|
|
const MaxNameLen = 50
|
|
const MaxShareNameLen = 150
|
|
const MaxRendererLen = 50
|
|
const MaxRemoteAliasLen = 50
|
|
const PasswordUnchangedSentinel = "--unchanged--"
|
|
const DefaultPTERM = "MxM"
|
|
const MaxCommandLen = 4096
|
|
const MaxSignalLen = 12
|
|
const MaxSignalNum = 64
|
|
const MaxEvalDepth = 5
|
|
const MaxOpenAIAPITokenLen = 100
|
|
const MaxOpenAIModelLen = 100
|
|
|
|
const TermFontSizeMin = 8
|
|
const TermFontSizeMax = 24
|
|
|
|
const TsFormatStr = "2006-01-02 15:04:05"
|
|
|
|
const (
|
|
KwArgRenderer = "renderer"
|
|
KwArgView = "view"
|
|
KwArgState = "state"
|
|
KwArgTemplate = "template"
|
|
KwArgLang = "lang"
|
|
)
|
|
|
|
var ColorNames = []string{"yellow", "blue", "pink", "mint", "cyan", "violet", "orange", "green", "red", "white"}
|
|
var TabIcons = []string{"square", "sparkle", "fire", "ghost", "cloud", "compass", "crown", "droplet", "graduation-cap", "heart", "file"}
|
|
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
|
|
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
|
|
|
|
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
|
|
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
|
|
var GlobalCmds = []string{"session", "screen", "remote", "set", "client", "telemetry", "bookmark", "bookmarks"}
|
|
|
|
var SetVarNameMap map[string]string = map[string]string{
|
|
"tabcolor": "screen.tabcolor",
|
|
"tabicon": "screen.tabicon",
|
|
"pterm": "screen.pterm",
|
|
"anchor": "screen.anchor",
|
|
"focus": "screen.focus",
|
|
"line": "screen.line",
|
|
"index": "screen.index",
|
|
}
|
|
|
|
var SetVarScopes = []SetVarScope{
|
|
{ScopeName: "global", VarNames: []string{}},
|
|
{ScopeName: "client", VarNames: []string{"telemetry"}},
|
|
{ScopeName: "session", VarNames: []string{"name", "pos"}},
|
|
{ScopeName: "screen", VarNames: []string{"name", "tabcolor", "tabicon", "pos", "pterm", "anchor", "focus", "line", "index"}},
|
|
{ScopeName: "line", VarNames: []string{}},
|
|
// connection = remote, remote = remoteinstance
|
|
{ScopeName: "connection", VarNames: []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}},
|
|
{ScopeName: "remote", VarNames: []string{}},
|
|
}
|
|
|
|
var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9._@-]*)@([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$")
|
|
var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$")
|
|
var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$")
|
|
var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$")
|
|
var positionRe = regexp.MustCompile("^((S?\\+|E?-)?[0-9]+|(\\+|-|S|E))$")
|
|
var wsRe = regexp.MustCompile("\\s+")
|
|
var sigNameRe = regexp.MustCompile("^((SIG[A-Z0-9]+)|(\\d+))$")
|
|
|
|
type contextType string
|
|
|
|
var historyContextKey = contextType("history")
|
|
var depthContextKey = contextType("depth")
|
|
|
|
type SetVarScope struct {
|
|
ScopeName string
|
|
VarNames []string
|
|
}
|
|
|
|
type historyContextType struct {
|
|
LineId string
|
|
LineNum int64
|
|
RemotePtr *sstore.RemotePtrType
|
|
}
|
|
|
|
type MetaCmdFnType = func(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error)
|
|
type MetaCmdEntryType struct {
|
|
IsAlias bool
|
|
Fn MetaCmdFnType
|
|
}
|
|
|
|
var MetaCmdFnMap = make(map[string]MetaCmdEntryType)
|
|
|
|
func init() {
|
|
registerCmdFn("run", RunCommand)
|
|
registerCmdFn("eval", EvalCommand)
|
|
registerCmdFn("comment", CommentCommand)
|
|
registerCmdFn("cr", CrCommand)
|
|
registerCmdFn("connect", CrCommand)
|
|
registerCmdFn("_compgen", CompGenCommand)
|
|
registerCmdFn("clear", ClearCommand)
|
|
registerCmdFn("reset", RemoteResetCommand)
|
|
registerCmdFn("signal", SignalCommand)
|
|
registerCmdFn("sync", SyncCommand)
|
|
|
|
registerCmdFn("session", SessionCommand)
|
|
registerCmdFn("session:open", SessionOpenCommand)
|
|
registerCmdAlias("session:new", SessionOpenCommand)
|
|
registerCmdFn("session:set", SessionSetCommand)
|
|
registerCmdAlias("session:delete", SessionDeleteCommand)
|
|
registerCmdFn("session:purge", SessionDeleteCommand)
|
|
registerCmdFn("session:archive", SessionArchiveCommand)
|
|
registerCmdFn("session:showall", SessionShowAllCommand)
|
|
registerCmdFn("session:show", SessionShowCommand)
|
|
registerCmdFn("session:openshared", SessionOpenSharedCommand)
|
|
|
|
registerCmdFn("screen", ScreenCommand)
|
|
registerCmdFn("screen:archive", ScreenArchiveCommand)
|
|
registerCmdFn("screen:purge", ScreenPurgeCommand)
|
|
registerCmdFn("screen:open", ScreenOpenCommand)
|
|
registerCmdAlias("screen:new", ScreenOpenCommand)
|
|
registerCmdFn("screen:set", ScreenSetCommand)
|
|
registerCmdFn("screen:showall", ScreenShowAllCommand)
|
|
registerCmdFn("screen:reset", ScreenResetCommand)
|
|
registerCmdFn("screen:webshare", ScreenWebShareCommand)
|
|
registerCmdFn("screen:reorder", ScreenReorderCommand)
|
|
|
|
registerCmdAlias("remote", RemoteCommand)
|
|
registerCmdFn("remote:show", RemoteShowCommand)
|
|
registerCmdFn("remote:showall", RemoteShowAllCommand)
|
|
registerCmdFn("remote:new", RemoteNewCommand)
|
|
registerCmdFn("remote:archive", RemoteArchiveCommand)
|
|
registerCmdFn("remote:set", RemoteSetCommand)
|
|
registerCmdFn("remote:disconnect", RemoteDisconnectCommand)
|
|
registerCmdFn("remote:connect", RemoteConnectCommand)
|
|
registerCmdFn("remote:install", RemoteInstallCommand)
|
|
registerCmdFn("remote:installcancel", RemoteInstallCancelCommand)
|
|
registerCmdFn("remote:reset", RemoteResetCommand)
|
|
|
|
registerCmdFn("screen:resize", ScreenResizeCommand)
|
|
|
|
registerCmdFn("line", LineCommand)
|
|
registerCmdFn("line:show", LineShowCommand)
|
|
registerCmdFn("line:star", LineStarCommand)
|
|
registerCmdFn("line:bookmark", LineBookmarkCommand)
|
|
registerCmdFn("line:pin", LinePinCommand)
|
|
registerCmdFn("line:archive", LineArchiveCommand)
|
|
registerCmdFn("line:purge", LinePurgeCommand)
|
|
registerCmdFn("line:setheight", LineSetHeightCommand)
|
|
registerCmdFn("line:view", LineViewCommand)
|
|
registerCmdFn("line:set", LineSetCommand)
|
|
|
|
registerCmdFn("client", ClientCommand)
|
|
registerCmdFn("client:show", ClientShowCommand)
|
|
registerCmdFn("client:set", ClientSetCommand)
|
|
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
|
|
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
|
|
|
|
registerCmdFn("telemetry", TelemetryCommand)
|
|
registerCmdFn("telemetry:on", TelemetryOnCommand)
|
|
registerCmdFn("telemetry:off", TelemetryOffCommand)
|
|
registerCmdFn("telemetry:send", TelemetrySendCommand)
|
|
registerCmdFn("telemetry:show", TelemetryShowCommand)
|
|
|
|
registerCmdFn("releasecheck", ReleaseCheckCommand)
|
|
registerCmdFn("releasecheck:autoon", ReleaseCheckOnCommand)
|
|
registerCmdFn("releasecheck:autooff", ReleaseCheckOffCommand)
|
|
|
|
registerCmdFn("history", HistoryCommand)
|
|
registerCmdFn("history:viewall", HistoryViewAllCommand)
|
|
registerCmdFn("history:purge", HistoryPurgeCommand)
|
|
|
|
registerCmdFn("bookmarks:show", BookmarksShowCommand)
|
|
|
|
registerCmdFn("bookmark:set", BookmarkSetCommand)
|
|
registerCmdFn("bookmark:delete", BookmarkDeleteCommand)
|
|
|
|
registerCmdFn("chat", OpenAICommand)
|
|
|
|
registerCmdFn("_killserver", KillServerCommand)
|
|
|
|
registerCmdFn("set", SetCommand)
|
|
|
|
registerCmdFn("view:stat", ViewStatCommand)
|
|
registerCmdFn("view:test", ViewTestCommand)
|
|
|
|
registerCmdFn("edit:test", EditTestCommand)
|
|
|
|
// CodeEditCommand is overloaded to do codeedit and codeview
|
|
registerCmdFn("codeedit", CodeEditCommand)
|
|
registerCmdFn("codeview", CodeEditCommand)
|
|
|
|
registerCmdFn("imageview", ImageViewCommand)
|
|
registerCmdFn("mdview", MarkdownViewCommand)
|
|
registerCmdFn("markdownview", MarkdownViewCommand)
|
|
|
|
registerCmdFn("csvview", CSVViewCommand)
|
|
}
|
|
|
|
func getValidCommands() []string {
|
|
var rtn []string
|
|
for key, val := range MetaCmdFnMap {
|
|
if val.IsAlias {
|
|
continue
|
|
}
|
|
rtn = append(rtn, "/"+key)
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func registerCmdFn(cmdName string, fn MetaCmdFnType) {
|
|
MetaCmdFnMap[cmdName] = MetaCmdEntryType{Fn: fn}
|
|
}
|
|
|
|
func registerCmdAlias(cmdName string, fn MetaCmdFnType) {
|
|
MetaCmdFnMap[cmdName] = MetaCmdEntryType{IsAlias: true, Fn: fn}
|
|
}
|
|
|
|
func GetCmdStr(pk *scpacket.FeCommandPacketType) string {
|
|
if pk.MetaSubCmd == "" {
|
|
return pk.MetaCmd
|
|
}
|
|
return pk.MetaCmd + ":" + pk.MetaSubCmd
|
|
}
|
|
|
|
func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
metaCmd := SubMetaCmd(pk.MetaCmd)
|
|
var cmdName string
|
|
if pk.MetaSubCmd == "" {
|
|
cmdName = metaCmd
|
|
} else {
|
|
cmdName = fmt.Sprintf("%s:%s", pk.MetaCmd, pk.MetaSubCmd)
|
|
}
|
|
entry := MetaCmdFnMap[cmdName]
|
|
if entry.Fn == nil {
|
|
if MetaCmdFnMap[metaCmd].Fn != nil {
|
|
return nil, fmt.Errorf("invalid /%s subcommand '%s'", metaCmd, pk.MetaSubCmd)
|
|
}
|
|
return nil, fmt.Errorf("invalid command '/%s', no handler", cmdName)
|
|
}
|
|
return entry.Fn(ctx, pk)
|
|
}
|
|
|
|
func firstArg(pk *scpacket.FeCommandPacketType) string {
|
|
if len(pk.Args) == 0 {
|
|
return ""
|
|
}
|
|
return pk.Args[0]
|
|
}
|
|
|
|
func argN(pk *scpacket.FeCommandPacketType, n int) string {
|
|
if len(pk.Args) <= n {
|
|
return ""
|
|
}
|
|
return pk.Args[n]
|
|
}
|
|
|
|
func resolveBool(arg string, def bool) bool {
|
|
if arg == "" {
|
|
return def
|
|
}
|
|
if arg == "0" || arg == "false" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func defaultStr(arg string, def string) string {
|
|
if arg == "" {
|
|
return def
|
|
}
|
|
return arg
|
|
}
|
|
|
|
func resolveFile(arg string) (string, error) {
|
|
if arg == "" {
|
|
return "", nil
|
|
}
|
|
fileName := base.ExpandHomeDir(arg)
|
|
if !strings.HasPrefix(fileName, "/") {
|
|
return "", fmt.Errorf("must be absolute, cannot be a relative path")
|
|
}
|
|
fd, err := os.Open(fileName)
|
|
if fd != nil {
|
|
fd.Close()
|
|
}
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot open file: %v", err)
|
|
}
|
|
return fileName, nil
|
|
}
|
|
|
|
func resolvePosInt(arg string, def int) (int, error) {
|
|
if arg == "" {
|
|
return def, nil
|
|
}
|
|
ival, err := strconv.Atoi(arg)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if ival <= 0 {
|
|
return 0, fmt.Errorf("must be greater than 0")
|
|
}
|
|
return ival, nil
|
|
}
|
|
|
|
func isAllDigits(arg string) bool {
|
|
if len(arg) == 0 {
|
|
return false
|
|
}
|
|
for i := 0; i < len(arg); i++ {
|
|
if arg[i] >= '0' && arg[i] <= '9' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func resolveNonNegInt(arg string, def int) (int, error) {
|
|
if arg == "" {
|
|
return def, nil
|
|
}
|
|
ival, err := strconv.Atoi(arg)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if ival < 0 {
|
|
return 0, fmt.Errorf("cannot be negative")
|
|
}
|
|
return ival, nil
|
|
}
|
|
|
|
var histExpansionRe = regexp.MustCompile(`^!(\d+)$`)
|
|
|
|
func doCmdHistoryExpansion(ctx context.Context, ids resolvedIds, cmdStr string) (string, error) {
|
|
if !strings.HasPrefix(cmdStr, "!") {
|
|
return "", nil
|
|
}
|
|
if strings.HasPrefix(cmdStr, "! ") {
|
|
return "", nil
|
|
}
|
|
if cmdStr == "!!" {
|
|
return doHistoryExpansion(ctx, ids, -1)
|
|
}
|
|
if strings.HasPrefix(cmdStr, "!-") {
|
|
return "", fmt.Errorf("wave does not support negative history offsets, use a stable positive history offset instead: '![linenum]'")
|
|
}
|
|
m := histExpansionRe.FindStringSubmatch(cmdStr)
|
|
if m == nil {
|
|
return "", fmt.Errorf("unsupported history substitution, can use '!!' or '![linenum]'")
|
|
}
|
|
ival, err := strconv.Atoi(m[1])
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid history expansion")
|
|
}
|
|
return doHistoryExpansion(ctx, ids, ival)
|
|
}
|
|
|
|
func doHistoryExpansion(ctx context.Context, ids resolvedIds, hnum int) (string, error) {
|
|
if hnum == 0 {
|
|
return "", fmt.Errorf("invalid history expansion, cannot expand line number '0'")
|
|
}
|
|
if hnum < -1 {
|
|
return "", fmt.Errorf("invalid history expansion, cannot expand negative history offsets")
|
|
}
|
|
foundHistoryNum := hnum
|
|
if hnum == -1 {
|
|
var err error
|
|
foundHistoryNum, err = sstore.GetLastHistoryLineNum(ctx, ids.ScreenId)
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot expand history, error finding last history item: %v", err)
|
|
}
|
|
if foundHistoryNum == 0 {
|
|
return "", fmt.Errorf("cannot expand history, no last history item")
|
|
}
|
|
}
|
|
hitem, err := sstore.GetHistoryItemByLineNum(ctx, ids.ScreenId, foundHistoryNum)
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot get history item '%d': %v", foundHistoryNum, err)
|
|
}
|
|
if hitem == nil {
|
|
return "", fmt.Errorf("cannot expand history, history item '%d' not found", foundHistoryNum)
|
|
}
|
|
return hitem.CmdStr, nil
|
|
}
|
|
|
|
func getEvalDepth(ctx context.Context) int {
|
|
depthVal := ctx.Value(depthContextKey)
|
|
if depthVal == nil {
|
|
return 0
|
|
}
|
|
return depthVal.(int)
|
|
}
|
|
|
|
func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/run error: %w", err)
|
|
}
|
|
runPacket := packet.MakeRunPacket()
|
|
runPacket.ReqId = uuid.New().String()
|
|
runPacket.CK = base.MakeCommandKey(ids.ScreenId, scbase.GenWaveUUID())
|
|
runPacket.UsePty = true
|
|
ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM)
|
|
runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/sync error, invalid 'wterm' value %q: %v", ptermVal, err)
|
|
}
|
|
runPacket.Command = ":"
|
|
runPacket.ReturnState = true
|
|
cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, runPacket)
|
|
if callback != nil {
|
|
defer callback()
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd.RawCmdStr = pk.GetRawStr()
|
|
update, err := addLineForCmd(ctx, "/sync", true, ids, cmd, "terminal", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
update.Interactive = pk.Interactive
|
|
sstore.MainBus.SendScreenUpdate(ids.ScreenId, update)
|
|
return nil, nil
|
|
}
|
|
|
|
func getRendererArg(pk *scpacket.FeCommandPacketType) (string, error) {
|
|
rval := pk.Kwargs[KwArgView]
|
|
if rval == "" {
|
|
rval = pk.Kwargs[KwArgRenderer]
|
|
}
|
|
if rval == "" {
|
|
return "", nil
|
|
}
|
|
err := validateRenderer(rval)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return rval, nil
|
|
}
|
|
|
|
func getTemplateArg(pk *scpacket.FeCommandPacketType) (string, error) {
|
|
rval := pk.Kwargs[KwArgTemplate]
|
|
if rval == "" {
|
|
return "", nil
|
|
}
|
|
// TODO validate
|
|
return rval, nil
|
|
}
|
|
|
|
func getLangArg(pk *scpacket.FeCommandPacketType) (string, error) {
|
|
// TODO better error checking
|
|
if len(pk.Kwargs[KwArgLang]) > 50 {
|
|
return "", nil // TODO return error, don't fail silently
|
|
}
|
|
return pk.Kwargs[KwArgLang], nil
|
|
}
|
|
|
|
func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/run error: %w", err)
|
|
}
|
|
renderer, err := getRendererArg(pk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/run error, invalid view/renderer: %w", err)
|
|
}
|
|
templateArg, err := getTemplateArg(pk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/run error, invalid template: %w", err)
|
|
}
|
|
langArg, err := getLangArg(pk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/run error, invalid lang: %w", err)
|
|
}
|
|
cmdStr := firstArg(pk)
|
|
expandedCmdStr, err := doCmdHistoryExpansion(ctx, ids, cmdStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if expandedCmdStr != "" {
|
|
newPk := scpacket.MakeFeCommandPacket()
|
|
newPk.MetaCmd = "eval"
|
|
newPk.Args = []string{expandedCmdStr}
|
|
newPk.Kwargs = pk.Kwargs
|
|
newPk.RawStr = pk.RawStr
|
|
newPk.UIContext = pk.UIContext
|
|
newPk.Interactive = pk.Interactive
|
|
evalDepth := getEvalDepth(ctx)
|
|
ctxWithDepth := context.WithValue(ctx, depthContextKey, evalDepth+1)
|
|
return EvalCommand(ctxWithDepth, newPk)
|
|
}
|
|
isRtnStateCmd := IsReturnStateCommand(cmdStr)
|
|
// runPacket.State is set in remote.RunCommand()
|
|
runPacket := packet.MakeRunPacket()
|
|
runPacket.ReqId = uuid.New().String()
|
|
runPacket.CK = base.MakeCommandKey(ids.ScreenId, scbase.GenWaveUUID())
|
|
runPacket.UsePty = true
|
|
ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM)
|
|
runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, ptermVal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/run error, invalid 'pterm' value %q: %v", ptermVal, err)
|
|
}
|
|
runPacket.Command = strings.TrimSpace(cmdStr)
|
|
runPacket.ReturnState = resolveBool(pk.Kwargs["rtnstate"], isRtnStateCmd)
|
|
cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, runPacket)
|
|
if callback != nil {
|
|
defer callback()
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd.RawCmdStr = pk.GetRawStr()
|
|
lineState := make(map[string]any)
|
|
if templateArg != "" {
|
|
lineState[sstore.LineState_Template] = templateArg
|
|
}
|
|
if langArg != "" {
|
|
lineState[sstore.LineState_Lang] = langArg
|
|
}
|
|
update, err := addLineForCmd(ctx, "/run", true, ids, cmd, renderer, lineState)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
update.Interactive = pk.Interactive
|
|
sstore.MainBus.SendScreenUpdate(ids.ScreenId, update)
|
|
return nil, nil
|
|
}
|
|
|
|
func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, historyContext historyContextType, isMetaCmd bool, hadError bool) error {
|
|
cmdStr := firstArg(pk)
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
isIncognito, err := sstore.IsIncognitoScreen(ctx, ids.SessionId, ids.ScreenId)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot add to history, error looking up incognito status of screen: %v", err)
|
|
}
|
|
hitem := &sstore.HistoryItemType{
|
|
HistoryId: scbase.GenWaveUUID(),
|
|
Ts: time.Now().UnixMilli(),
|
|
UserId: DefaultUserId,
|
|
SessionId: ids.SessionId,
|
|
ScreenId: ids.ScreenId,
|
|
LineId: historyContext.LineId,
|
|
LineNum: historyContext.LineNum,
|
|
HadError: hadError,
|
|
CmdStr: cmdStr,
|
|
IsMetaCmd: isMetaCmd,
|
|
Incognito: isIncognito,
|
|
}
|
|
if !isMetaCmd && historyContext.RemotePtr != nil {
|
|
hitem.Remote = *historyContext.RemotePtr
|
|
}
|
|
err = sstore.InsertHistoryItem(ctx, hitem)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("usage: /eval [command], no command passed to eval")
|
|
}
|
|
if len(pk.Args[0]) > MaxCommandLen {
|
|
return nil, fmt.Errorf("command length too long len:%d, max:%d", len(pk.Args[0]), MaxCommandLen)
|
|
}
|
|
evalDepth := getEvalDepth(ctx)
|
|
if pk.Interactive && evalDepth == 0 {
|
|
err := sstore.UpdateCurrentActivity(ctx, sstore.ActivityUpdate{NumCommands: 1})
|
|
if err != nil {
|
|
log.Printf("[error] incrementing activity numcommands: %v\n", err)
|
|
// fall through (non-fatal error)
|
|
}
|
|
}
|
|
if evalDepth > MaxEvalDepth {
|
|
return nil, fmt.Errorf("alias/history expansion max-depth exceeded")
|
|
}
|
|
var historyContext historyContextType
|
|
ctxWithHistory := context.WithValue(ctx, historyContextKey, &historyContext)
|
|
var update sstore.UpdatePacket
|
|
newPk, rtnErr := EvalMetaCommand(ctxWithHistory, pk)
|
|
if rtnErr == nil {
|
|
update, rtnErr = HandleCommand(ctxWithHistory, newPk)
|
|
}
|
|
if !resolveBool(pk.Kwargs["nohist"], false) {
|
|
err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil))
|
|
if err != nil {
|
|
log.Printf("[error] adding to history: %v\n", err)
|
|
// fall through (non-fatal error)
|
|
}
|
|
}
|
|
return update, rtnErr
|
|
}
|
|
|
|
func ScreenArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session) // don't force R_Screen
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:archive cannot archive screen: %w", err)
|
|
}
|
|
screenId := ids.ScreenId
|
|
if len(pk.Args) > 0 {
|
|
ri, err := resolveSessionScreen(ctx, ids.SessionId, pk.Args[0], ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:archive cannot resolve screen arg: %v", err)
|
|
}
|
|
screenId = ri.Id
|
|
}
|
|
if screenId == "" {
|
|
return nil, fmt.Errorf("/screen:archive no active screen or screen arg passed")
|
|
}
|
|
archiveVal := true
|
|
if len(pk.Args) > 1 {
|
|
archiveVal = resolveBool(pk.Args[1], true)
|
|
}
|
|
var update sstore.UpdatePacket
|
|
if archiveVal {
|
|
update, err = sstore.ArchiveScreen(ctx, ids.SessionId, screenId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return update, nil
|
|
} else {
|
|
log.Printf("unarchive screen %s\n", screenId)
|
|
err = sstore.UnArchiveScreen(ctx, ids.SessionId, screenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:archive cannot un-archive screen: %v", err)
|
|
}
|
|
screen, err := sstore.GetScreenById(ctx, screenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:archive cannot get updated screen obj: %v", err)
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Screens: []*sstore.ScreenType{screen},
|
|
}
|
|
return update, nil
|
|
}
|
|
}
|
|
|
|
func ScreenPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session) // don't force R_Screen
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:purge cannot purge screen: %w", err)
|
|
}
|
|
screenId := ids.ScreenId
|
|
if len(pk.Args) > 0 {
|
|
ri, err := resolveSessionScreen(ctx, ids.SessionId, pk.Args[0], ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:purge cannot resolve screen arg: %v", err)
|
|
}
|
|
screenId = ri.Id
|
|
}
|
|
if screenId == "" {
|
|
return nil, fmt.Errorf("/screen:purge no active screen or screen arg passed")
|
|
}
|
|
update, err := sstore.PurgeScreen(ctx, screenId, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func ScreenOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:open cannot open screen: %w", err)
|
|
}
|
|
activate := resolveBool(pk.Kwargs["activate"], true)
|
|
newName := pk.Kwargs["name"]
|
|
if newName != "" {
|
|
err := validateName(newName, "screen")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
update, err := sstore.InsertScreen(ctx, ids.SessionId, newName, sstore.ScreenCreateOpts{}, activate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func ScreenReorderCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
// Resolve the UI IDs for the session and screen
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Extract the screen ID and the new index from the packet
|
|
screenId := ids.ScreenId
|
|
newScreenIdxStr := pk.Kwargs["index"]
|
|
newScreenIdx, err := resolvePosInt(newScreenIdxStr, 1)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid new screen index: %v", err)
|
|
}
|
|
|
|
// Call SetScreenIdx to update the screen's index in the database
|
|
err = sstore.SetScreenIdx(ctx, ids.SessionId, screenId, newScreenIdx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating screen index: %v", err)
|
|
}
|
|
|
|
// Retrieve all session screens
|
|
screens, err := sstore.GetSessionScreens(ctx, ids.SessionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error retrieving updated screen: %v", err)
|
|
}
|
|
|
|
// Prepare the update packet to send back to the client
|
|
update := &sstore.ModelUpdate{
|
|
Screens: screens,
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: "screen indices updated successfully",
|
|
TimeoutMs: 2000,
|
|
},
|
|
}
|
|
|
|
return update, nil
|
|
}
|
|
|
|
func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var varsUpdated []string
|
|
var setNonAnchor bool // anchor does not receive an update
|
|
updateMap := make(map[string]interface{})
|
|
if pk.Kwargs["name"] != "" {
|
|
newName := pk.Kwargs["name"]
|
|
err = validateName(newName, "screen")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updateMap[sstore.ScreenField_Name] = newName
|
|
varsUpdated = append(varsUpdated, "name")
|
|
setNonAnchor = true
|
|
}
|
|
if pk.Kwargs["sharename"] != "" {
|
|
shareName := pk.Kwargs["sharename"]
|
|
err = validateShareName(shareName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updateMap[sstore.ScreenField_ShareName] = shareName
|
|
varsUpdated = append(varsUpdated, "sharename")
|
|
setNonAnchor = true
|
|
}
|
|
if pk.Kwargs["tabcolor"] != "" {
|
|
color := pk.Kwargs["tabcolor"]
|
|
err = validateColor(color, "screen tabcolor")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updateMap[sstore.ScreenField_TabColor] = color
|
|
varsUpdated = append(varsUpdated, "tabcolor")
|
|
setNonAnchor = true
|
|
}
|
|
if pk.Kwargs["tabicon"] != "" {
|
|
icon := pk.Kwargs["tabicon"]
|
|
updateMap[sstore.ScreenField_TabIcon] = icon
|
|
varsUpdated = append(varsUpdated, "tabicon")
|
|
setNonAnchor = true
|
|
}
|
|
if pk.Kwargs["pos"] != "" {
|
|
varsUpdated = append(varsUpdated, "pos")
|
|
setNonAnchor = true
|
|
}
|
|
if pk.Kwargs["focus"] != "" {
|
|
focusVal := pk.Kwargs["focus"]
|
|
if focusVal != sstore.ScreenFocusInput && focusVal != sstore.ScreenFocusCmd {
|
|
return nil, fmt.Errorf("/screen:set invalid focus argument %q, must be %s", focusVal, formatStrs([]string{sstore.ScreenFocusInput, sstore.ScreenFocusCmd}, "or", false))
|
|
}
|
|
varsUpdated = append(varsUpdated, "focus")
|
|
updateMap[sstore.ScreenField_Focus] = focusVal
|
|
setNonAnchor = true
|
|
}
|
|
if pk.Kwargs["line"] != "" {
|
|
screen, err := sstore.GetScreenById(ctx, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:set cannot get screen: %v", err)
|
|
}
|
|
var selectedLineStr string
|
|
if screen.SelectedLine > 0 {
|
|
selectedLineStr = strconv.Itoa(int(screen.SelectedLine))
|
|
}
|
|
ritem, err := resolveLine(ctx, screen.SessionId, screen.ScreenId, pk.Kwargs["line"], selectedLineStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:set error resolving line: %v", err)
|
|
}
|
|
if ritem == nil {
|
|
return nil, fmt.Errorf("/screen:set could not resolve line %q", pk.Kwargs["line"])
|
|
}
|
|
varsUpdated = append(varsUpdated, "line")
|
|
setNonAnchor = true
|
|
updateMap[sstore.ScreenField_SelectedLine] = ritem.Num
|
|
}
|
|
if pk.Kwargs["anchor"] != "" {
|
|
m := screenAnchorRe.FindStringSubmatch(pk.Kwargs["anchor"])
|
|
if m == nil {
|
|
return nil, fmt.Errorf("/screen:set invalid anchor argument (must be [line] or [line]:[offset])")
|
|
}
|
|
anchorLine, _ := strconv.Atoi(m[1])
|
|
varsUpdated = append(varsUpdated, "anchor")
|
|
updateMap[sstore.ScreenField_AnchorLine] = anchorLine
|
|
if m[2] != "" {
|
|
anchorOffset, _ := strconv.Atoi(m[2])
|
|
updateMap[sstore.ScreenField_AnchorOffset] = anchorOffset
|
|
} else {
|
|
updateMap[sstore.ScreenField_AnchorOffset] = 0
|
|
}
|
|
}
|
|
if len(varsUpdated) == 0 {
|
|
return nil, fmt.Errorf("/screen:set no updates, can set %s", formatStrs([]string{"name", "pos", "tabcolor", "tabicon", "focus", "anchor", "line", "sharename"}, "or", false))
|
|
}
|
|
screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating screen: %v", err)
|
|
}
|
|
if !setNonAnchor {
|
|
return nil, nil
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Screens: []*sstore.ScreenType{screen},
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("screen updated %s", formatStrs(varsUpdated, "and", false)),
|
|
TimeoutMs: 2000,
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen cannot switch to screen: %w", err)
|
|
}
|
|
firstArg := firstArg(pk)
|
|
if firstArg == "" {
|
|
return nil, fmt.Errorf("usage /screen [screen-name|screen-index|screen-id], no param specified")
|
|
}
|
|
ritem, err := resolveSessionScreen(ctx, ids.SessionId, firstArg, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
update, err := sstore.SwitchScreenById(ctx, ids.SessionId, ritem.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
var screenAnchorRe = regexp.MustCompile("^(\\d+)(?::(-?\\d+))?$")
|
|
|
|
func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mshell := ids.Remote.MShell
|
|
go mshell.RunInstall()
|
|
return &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
PtyRemoteId: ids.Remote.RemotePtr.RemoteId,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func RemoteInstallCancelCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mshell := ids.Remote.MShell
|
|
go mshell.CancelInstall()
|
|
return &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
PtyRemoteId: ids.Remote.RemotePtr.RemoteId,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func RemoteConnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
go ids.Remote.MShell.Launch(true)
|
|
return &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
PtyRemoteId: ids.Remote.RemotePtr.RemoteId,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func RemoteDisconnectCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
force := resolveBool(pk.Kwargs["force"], false)
|
|
go ids.Remote.MShell.Disconnect(force)
|
|
return &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
PtyRemoteId: ids.Remote.RemotePtr.RemoteId,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func makeRemoteEditUpdate_new(err error) sstore.UpdatePacket {
|
|
redit := &sstore.RemoteEditType{
|
|
RemoteEdit: true,
|
|
}
|
|
if err != nil {
|
|
redit.ErrorStr = err.Error()
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
RemoteEdit: redit,
|
|
},
|
|
}
|
|
return update
|
|
}
|
|
|
|
func makeRemoteEditErrorReturn_new(visual bool, err error) (sstore.UpdatePacket, error) {
|
|
if visual {
|
|
return makeRemoteEditUpdate_new(err), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
func makeRemoteEditUpdate_edit(ids resolvedIds, err error) sstore.UpdatePacket {
|
|
redit := &sstore.RemoteEditType{
|
|
RemoteEdit: true,
|
|
}
|
|
redit.RemoteId = ids.Remote.RemotePtr.RemoteId
|
|
if ids.Remote.RemoteCopy.SSHOpts != nil {
|
|
redit.KeyStr = ids.Remote.RemoteCopy.SSHOpts.SSHIdentity
|
|
redit.HasPassword = (ids.Remote.RemoteCopy.SSHOpts.SSHPassword != "")
|
|
}
|
|
if err != nil {
|
|
redit.ErrorStr = err.Error()
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
RemoteEdit: redit,
|
|
},
|
|
}
|
|
return update
|
|
}
|
|
|
|
func makeRemoteEditErrorReturn_edit(ids resolvedIds, visual bool, err error) (sstore.UpdatePacket, error) {
|
|
if visual {
|
|
return makeRemoteEditUpdate_edit(ids, err), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
type RemoteEditArgs struct {
|
|
CanonicalName string
|
|
SSHOpts *sstore.SSHOpts
|
|
ConnectMode string
|
|
Alias string
|
|
AutoInstall bool
|
|
SSHPassword string
|
|
SSHKeyFile string
|
|
Color string
|
|
EditMap map[string]interface{}
|
|
}
|
|
|
|
func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal bool) (*RemoteEditArgs, error) {
|
|
var canonicalName string
|
|
var sshOpts *sstore.SSHOpts
|
|
var isSudo bool
|
|
|
|
if isNew {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/remote:new must specify user@host argument (set visual=1 to edit in UI)")
|
|
}
|
|
userHost := pk.Args[0]
|
|
m := userHostRe.FindStringSubmatch(userHost)
|
|
if m == nil {
|
|
return nil, fmt.Errorf("invalid format of user@host argument")
|
|
}
|
|
sudoStr, remoteUser, remoteHost, remotePortStr := m[1], m[2], m[3], m[4]
|
|
var uhPort int
|
|
if remotePortStr != "" {
|
|
var err error
|
|
uhPort, err = strconv.Atoi(remotePortStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid port specified on user@host argument")
|
|
}
|
|
}
|
|
if sudoStr != "" {
|
|
isSudo = true
|
|
}
|
|
if pk.Kwargs["sudo"] != "" {
|
|
sudoArg := resolveBool(pk.Kwargs["sudo"], false)
|
|
if isSudo && !sudoArg {
|
|
return nil, fmt.Errorf("invalid 'sudo' argument, with sudo kw arg set to false")
|
|
}
|
|
if !isSudo && sudoArg {
|
|
isSudo = true
|
|
}
|
|
}
|
|
sshOpts = &sstore.SSHOpts{
|
|
Local: false,
|
|
SSHHost: remoteHost,
|
|
SSHUser: remoteUser,
|
|
IsSudo: isSudo,
|
|
}
|
|
portVal, err := resolvePosInt(pk.Kwargs["port"], 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid port %q: %v", pk.Kwargs["port"], err)
|
|
}
|
|
if portVal != 0 && uhPort != 0 && portVal != uhPort {
|
|
return nil, fmt.Errorf("invalid port argument, does not match port specified in 'user@host:port' argument")
|
|
}
|
|
if portVal == 0 && uhPort != 0 {
|
|
portVal = uhPort
|
|
}
|
|
sshOpts.SSHPort = portVal
|
|
canonicalName = remoteUser + "@" + remoteHost
|
|
if isSudo {
|
|
canonicalName = "sudo@" + canonicalName
|
|
}
|
|
} else {
|
|
if pk.Kwargs["sudo"] != "" {
|
|
return nil, fmt.Errorf("cannot update 'sudo' value")
|
|
}
|
|
if pk.Kwargs["port"] != "" {
|
|
return nil, fmt.Errorf("cannot update 'port' value")
|
|
}
|
|
}
|
|
alias := pk.Kwargs["alias"]
|
|
if alias != "" {
|
|
if len(alias) > MaxRemoteAliasLen {
|
|
return nil, fmt.Errorf("alias too long, max length = %d", MaxRemoteAliasLen)
|
|
}
|
|
if !remoteAliasRe.MatchString(alias) {
|
|
return nil, fmt.Errorf("invalid alias format")
|
|
}
|
|
}
|
|
var connectMode string
|
|
if isNew {
|
|
connectMode = sstore.ConnectModeAuto
|
|
}
|
|
if pk.Kwargs["connectmode"] != "" {
|
|
connectMode = pk.Kwargs["connectmode"]
|
|
}
|
|
if connectMode != "" && !sstore.IsValidConnectMode(connectMode) {
|
|
err := fmt.Errorf("invalid connectmode %q: valid modes are %s", connectMode, formatStrs([]string{sstore.ConnectModeStartup, sstore.ConnectModeAuto, sstore.ConnectModeManual}, "or", false))
|
|
return nil, err
|
|
}
|
|
keyFile, err := resolveFile(pk.Kwargs["key"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid ssh keyfile %q: %v", pk.Kwargs["key"], err)
|
|
}
|
|
color := pk.Kwargs["color"]
|
|
if color != "" {
|
|
err := validateRemoteColor(color, "remote color")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
sshPassword := pk.Kwargs["password"]
|
|
if sshOpts != nil {
|
|
sshOpts.SSHIdentity = keyFile
|
|
sshOpts.SSHPassword = sshPassword
|
|
}
|
|
|
|
// set up editmap
|
|
editMap := make(map[string]interface{})
|
|
if _, found := pk.Kwargs[sstore.RemoteField_Alias]; found {
|
|
editMap[sstore.RemoteField_Alias] = alias
|
|
}
|
|
if connectMode != "" {
|
|
if isLocal {
|
|
return nil, fmt.Errorf("Cannot edit connect mode for 'local' remote")
|
|
}
|
|
editMap[sstore.RemoteField_ConnectMode] = connectMode
|
|
}
|
|
if _, found := pk.Kwargs["key"]; found {
|
|
if isLocal {
|
|
return nil, fmt.Errorf("Cannot edit ssh key file for 'local' remote")
|
|
}
|
|
editMap[sstore.RemoteField_SSHKey] = keyFile
|
|
}
|
|
if _, found := pk.Kwargs[sstore.RemoteField_Color]; found {
|
|
editMap[sstore.RemoteField_Color] = color
|
|
}
|
|
if _, found := pk.Kwargs["password"]; found && pk.Kwargs["password"] != PasswordUnchangedSentinel {
|
|
if isLocal {
|
|
return nil, fmt.Errorf("Cannot edit ssh password for 'local' remote")
|
|
}
|
|
editMap[sstore.RemoteField_SSHPassword] = sshPassword
|
|
}
|
|
|
|
return &RemoteEditArgs{
|
|
SSHOpts: sshOpts,
|
|
ConnectMode: connectMode,
|
|
Alias: alias,
|
|
AutoInstall: true,
|
|
CanonicalName: canonicalName,
|
|
SSHKeyFile: keyFile,
|
|
SSHPassword: sshPassword,
|
|
Color: color,
|
|
EditMap: editMap,
|
|
}, nil
|
|
}
|
|
|
|
func RemoteNewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
visualEdit := resolveBool(pk.Kwargs["visual"], false)
|
|
isSubmitted := resolveBool(pk.Kwargs["submit"], false)
|
|
if visualEdit && !isSubmitted && len(pk.Args) == 0 {
|
|
return makeRemoteEditUpdate_new(nil), nil
|
|
}
|
|
editArgs, err := parseRemoteEditArgs(true, pk, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/remote:new %v", err)
|
|
}
|
|
r := &sstore.RemoteType{
|
|
RemoteId: scbase.GenWaveUUID(),
|
|
RemoteType: sstore.RemoteTypeSsh,
|
|
RemoteAlias: editArgs.Alias,
|
|
RemoteCanonicalName: editArgs.CanonicalName,
|
|
RemoteUser: editArgs.SSHOpts.SSHUser,
|
|
RemoteHost: editArgs.SSHOpts.SSHHost,
|
|
ConnectMode: editArgs.ConnectMode,
|
|
AutoInstall: editArgs.AutoInstall,
|
|
SSHOpts: editArgs.SSHOpts,
|
|
}
|
|
if editArgs.Color != "" {
|
|
r.RemoteOpts = &sstore.RemoteOptsType{Color: editArgs.Color}
|
|
}
|
|
err = remote.AddRemote(ctx, r, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot create remote %q: %v", r.RemoteCanonicalName, err)
|
|
}
|
|
// SUCCESS
|
|
return &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
PtyRemoteId: r.RemoteId,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
visualEdit := resolveBool(pk.Kwargs["visual"], false)
|
|
isSubmitted := resolveBool(pk.Kwargs["submit"], false)
|
|
editArgs, err := parseRemoteEditArgs(false, pk, ids.Remote.MShell.IsLocal())
|
|
if err != nil {
|
|
return makeRemoteEditErrorReturn_edit(ids, visualEdit, fmt.Errorf("/remote:new %v", err))
|
|
}
|
|
if visualEdit && !isSubmitted && len(editArgs.EditMap) == 0 {
|
|
return makeRemoteEditUpdate_edit(ids, nil), nil
|
|
}
|
|
if !visualEdit && len(editArgs.EditMap) == 0 {
|
|
return nil, fmt.Errorf("/remote:set no updates, can set %s. (set visual=1 to edit in UI)", formatStrs(RemoteSetArgs, "or", false))
|
|
}
|
|
err = ids.Remote.MShell.UpdateRemote(ctx, editArgs.EditMap)
|
|
if err != nil {
|
|
return makeRemoteEditErrorReturn_edit(ids, visualEdit, fmt.Errorf("/remote:new error updating remote: %v", err))
|
|
}
|
|
if visualEdit {
|
|
return &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
PtyRemoteId: ids.Remote.RemoteCopy.RemoteId,
|
|
},
|
|
}, nil
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("remote %q updated", ids.Remote.DisplayName),
|
|
TimeoutMs: 2000,
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func RemoteShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state := ids.Remote.RState
|
|
return &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
PtyRemoteId: state.RemoteId,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func RemoteShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
stateArr := remote.GetAllRemoteRuntimeState()
|
|
var buf bytes.Buffer
|
|
for _, rstate := range stateArr {
|
|
var name string
|
|
if rstate.RemoteAlias == "" {
|
|
name = rstate.RemoteCanonicalName
|
|
} else {
|
|
name = fmt.Sprintf("%s (%s)", rstate.RemoteCanonicalName, rstate.RemoteAlias)
|
|
}
|
|
buf.WriteString(fmt.Sprintf("%-12s %-5s %8s %s\n", rstate.Status, rstate.RemoteType, rstate.RemoteId[0:8], name))
|
|
}
|
|
return &sstore.ModelUpdate{
|
|
RemoteView: &sstore.RemoteViewType{
|
|
RemoteShowAll: true,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session)
|
|
screenArr, err := sstore.GetSessionScreens(ctx, ids.SessionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:showall error getting screen list: %v", err)
|
|
}
|
|
var buf bytes.Buffer
|
|
for _, screen := range screenArr {
|
|
var archivedStr string
|
|
if screen.Archived {
|
|
archivedStr = " (archived)"
|
|
}
|
|
screenIdxStr := "-"
|
|
if screen.ScreenIdx != 0 {
|
|
screenIdxStr = strconv.Itoa(int(screen.ScreenIdx))
|
|
}
|
|
outStr := fmt.Sprintf("%-30s %s %s\n", screen.Name+archivedStr, screen.ScreenId, screenIdxStr)
|
|
buf.WriteString(outStr)
|
|
}
|
|
return &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: fmt.Sprintf("all screens for session"),
|
|
InfoLines: splitLinesForInfo(buf.String()),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func ScreenResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
localRemote := remote.GetLocalRemote()
|
|
if localRemote == nil {
|
|
return nil, fmt.Errorf("error getting local remote (not found)")
|
|
}
|
|
rptr := sstore.RemotePtrType{RemoteId: localRemote.RemoteId}
|
|
sessionUpdate := &sstore.SessionType{SessionId: ids.SessionId}
|
|
ris, err := sstore.ScreenReset(ctx, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error resetting screen: %v", err)
|
|
}
|
|
sessionUpdate.Remotes = append(sessionUpdate.Remotes, ris...)
|
|
err = sstore.UpdateCurRemote(ctx, ids.ScreenId, rptr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot reset screen remote back to local: %w", err)
|
|
}
|
|
outputStr := "reset screen state (all remote state reset)"
|
|
cmd, err := makeStaticCmd(ctx, "screen:reset", ids, pk.GetRawStr(), []byte(outputStr))
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update, err := addLineForCmd(ctx, "/screen:reset", false, ids, cmd, "", nil)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update.Interactive = pk.Interactive
|
|
update.Sessions = []*sstore.SessionType{sessionUpdate}
|
|
return update, nil
|
|
}
|
|
|
|
func RemoteArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = remote.ArchiveRemote(ctx, ids.Remote.RemotePtr.RemoteId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("archiving remote: %v", err)
|
|
}
|
|
update := sstore.InfoMsgUpdate("remote [%s] archived", ids.Remote.DisplayName)
|
|
localRemote := remote.GetLocalRemote()
|
|
rptr := sstore.RemotePtrType{RemoteId: localRemote.GetRemoteId()}
|
|
err = sstore.UpdateCurRemote(ctx, ids.ScreenId, rptr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot switch remote back to local: %w", err)
|
|
}
|
|
screen, err := sstore.GetScreenById(ctx, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get updated screen: %w", err)
|
|
}
|
|
update.Screens = []*sstore.ScreenType{screen}
|
|
return update, nil
|
|
}
|
|
|
|
func RemoteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
return nil, fmt.Errorf("/remote requires a subcommand: %s", formatStrs([]string{"show"}, "or", false))
|
|
}
|
|
|
|
func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids resolvedIds) (sstore.UpdatePacket, error) {
|
|
var buf bytes.Buffer
|
|
riArr, err := sstore.GetRIsForScreen(ctx, ids.SessionId, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get remote instances: %w", err)
|
|
}
|
|
rmap := remote.GetRemoteMap()
|
|
for _, ri := range riArr {
|
|
rptr := sstore.RemotePtrType{RemoteId: ri.RemoteId, Name: ri.Name}
|
|
msh := rmap[ri.RemoteId]
|
|
if msh == nil {
|
|
continue
|
|
}
|
|
baseDisplayName := msh.GetDisplayName()
|
|
displayName := rptr.GetDisplayName(baseDisplayName)
|
|
cwdStr := "-"
|
|
if ri.FeState["cwd"] != "" {
|
|
cwdStr = ri.FeState["cwd"]
|
|
}
|
|
buf.WriteString(fmt.Sprintf("%-30s %-50s\n", displayName, cwdStr))
|
|
}
|
|
riBaseMap := make(map[string]bool)
|
|
for _, ri := range riArr {
|
|
if ri.Name == "" {
|
|
riBaseMap[ri.RemoteId] = true
|
|
}
|
|
}
|
|
for remoteId, msh := range rmap {
|
|
if riBaseMap[remoteId] {
|
|
continue
|
|
}
|
|
feState := msh.GetDefaultFeState()
|
|
if feState == nil {
|
|
continue
|
|
}
|
|
cwdStr := "-"
|
|
if feState["cwd"] != "" {
|
|
cwdStr = feState["cwd"]
|
|
}
|
|
buf.WriteString(fmt.Sprintf("%-30s %-50s (default)\n", msh.GetDisplayName(), cwdStr))
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoLines: splitLinesForInfo(buf.String()),
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func GetFullRemoteDisplayName(rptr *sstore.RemotePtrType, rstate *remote.RemoteRuntimeState) string {
|
|
if rptr == nil {
|
|
return "(invalid)"
|
|
}
|
|
if rstate.RemoteAlias != "" {
|
|
fullName := rstate.RemoteAlias
|
|
if rptr.Name != "" {
|
|
fullName = fullName + ":" + rptr.Name
|
|
}
|
|
return fmt.Sprintf("[%s] (%s)", fullName, rstate.RemoteCanonicalName)
|
|
} else {
|
|
if rptr.Name != "" {
|
|
return fmt.Sprintf("[%s:%s]", rstate.RemoteCanonicalName, rptr.Name)
|
|
}
|
|
return fmt.Sprintf("[%s]", rstate.RemoteCanonicalName)
|
|
}
|
|
}
|
|
|
|
func writeErrorToPty(cmd *sstore.CmdType, errStr string, outputPos int64) {
|
|
errPk := openai.CreateErrorPacket(errStr)
|
|
errBytes, err := packet.MarshalPacket(errPk)
|
|
if err != nil {
|
|
log.Printf("error writing error packet to openai response: %v\n", err)
|
|
return
|
|
}
|
|
errCtx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancelFn()
|
|
update, err := sstore.AppendToCmdPtyBlob(errCtx, cmd.ScreenId, cmd.LineId, errBytes, outputPos)
|
|
if err != nil {
|
|
log.Printf("error writing ptyupdate for openai response: %v\n", err)
|
|
return
|
|
}
|
|
sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update)
|
|
return
|
|
}
|
|
|
|
func writePacketToPty(ctx context.Context, cmd *sstore.CmdType, pk packet.PacketType, outputPos *int64) error {
|
|
outBytes, err := packet.MarshalPacket(pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
update, err := sstore.AppendToCmdPtyBlob(ctx, cmd.ScreenId, cmd.LineId, outBytes, *outputPos)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*outputPos += int64(len(outBytes))
|
|
sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update)
|
|
return nil
|
|
}
|
|
|
|
func doOpenAICompletion(cmd *sstore.CmdType, opts *sstore.OpenAIOptsType, prompt []sstore.OpenAIPromptMessageType) {
|
|
var outputPos int64
|
|
var hadError bool
|
|
startTime := time.Now()
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancelFn()
|
|
defer func() {
|
|
r := recover()
|
|
if r != nil {
|
|
panicMsg := fmt.Sprintf("panic: %v", r)
|
|
log.Printf("panic in doOpenAICompletion: %s\n", panicMsg)
|
|
writeErrorToPty(cmd, panicMsg, outputPos)
|
|
hadError = true
|
|
}
|
|
duration := time.Since(startTime)
|
|
cmdStatus := sstore.CmdStatusDone
|
|
var exitCode int
|
|
if hadError {
|
|
cmdStatus = sstore.CmdStatusError
|
|
exitCode = 1
|
|
}
|
|
ck := base.MakeCommandKey(cmd.ScreenId, cmd.LineId)
|
|
donePk := packet.MakeCmdDonePacket(ck)
|
|
donePk.Ts = time.Now().UnixMilli()
|
|
donePk.ExitCode = exitCode
|
|
donePk.DurationMs = duration.Milliseconds()
|
|
update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, donePk, cmdStatus)
|
|
if err != nil {
|
|
// nothing to do
|
|
log.Printf("error updating cmddoneinfo (in openai): %v\n", err)
|
|
return
|
|
}
|
|
sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update)
|
|
}()
|
|
respPks, err := openai.RunCompletion(ctx, opts, prompt)
|
|
if err != nil {
|
|
writeErrorToPty(cmd, fmt.Sprintf("error calling OpenAI API: %v", err), outputPos)
|
|
return
|
|
}
|
|
for _, pk := range respPks {
|
|
err = writePacketToPty(ctx, cmd, pk, &outputPos)
|
|
if err != nil {
|
|
writeErrorToPty(cmd, fmt.Sprintf("error writing response to ptybuffer: %v", err), outputPos)
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func doOpenAIStreamCompletion(cmd *sstore.CmdType, opts *sstore.OpenAIOptsType, prompt []sstore.OpenAIPromptMessageType) {
|
|
var outputPos int64
|
|
var hadError bool
|
|
startTime := time.Now()
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancelFn()
|
|
defer func() {
|
|
r := recover()
|
|
if r != nil {
|
|
panicMsg := fmt.Sprintf("panic: %v", r)
|
|
log.Printf("panic in doOpenAICompletion: %s\n", panicMsg)
|
|
writeErrorToPty(cmd, panicMsg, outputPos)
|
|
hadError = true
|
|
}
|
|
duration := time.Since(startTime)
|
|
cmdStatus := sstore.CmdStatusDone
|
|
var exitCode int
|
|
if hadError {
|
|
cmdStatus = sstore.CmdStatusError
|
|
exitCode = 1
|
|
}
|
|
ck := base.MakeCommandKey(cmd.ScreenId, cmd.LineId)
|
|
donePk := packet.MakeCmdDonePacket(ck)
|
|
donePk.Ts = time.Now().UnixMilli()
|
|
donePk.ExitCode = exitCode
|
|
donePk.DurationMs = duration.Milliseconds()
|
|
update, err := sstore.UpdateCmdDoneInfo(context.Background(), ck, donePk, cmdStatus)
|
|
if err != nil {
|
|
// nothing to do
|
|
log.Printf("error updating cmddoneinfo (in openai): %v\n", err)
|
|
return
|
|
}
|
|
sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update)
|
|
}()
|
|
ch, err := openai.RunCompletionStream(ctx, opts, prompt)
|
|
if err != nil {
|
|
writeErrorToPty(cmd, fmt.Sprintf("error calling OpenAI API: %v", err), outputPos)
|
|
return
|
|
}
|
|
for pk := range ch {
|
|
err = writePacketToPty(ctx, cmd, pk, &outputPos)
|
|
if err != nil {
|
|
writeErrorToPty(cmd, fmt.Sprintf("error writing response to ptybuffer: %v", err), outputPos)
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/%s error: %w", GetCmdStr(pk), err)
|
|
}
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
if clientData.OpenAIOpts == nil || clientData.OpenAIOpts.APIToken == "" {
|
|
return nil, fmt.Errorf("no openai API token found, configure in client settings")
|
|
}
|
|
opts := clientData.OpenAIOpts
|
|
if opts.Model == "" {
|
|
opts.Model = openai.DefaultModel
|
|
}
|
|
if opts.MaxTokens == 0 {
|
|
opts.MaxTokens = openai.DefaultMaxTokens
|
|
}
|
|
promptStr := firstArg(pk)
|
|
if promptStr == "" {
|
|
return nil, fmt.Errorf("openai error, prompt string is blank")
|
|
}
|
|
ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM)
|
|
pkTermOpts, err := GetUITermOpts(pk.UIContext.WinSize, ptermVal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("openai error, invalid 'pterm' value %q: %v", ptermVal, err)
|
|
}
|
|
termOpts := convertTermOpts(pkTermOpts)
|
|
cmd, err := makeDynCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), *termOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("openai error, cannot make dyn cmd")
|
|
}
|
|
line, err := sstore.AddOpenAILine(ctx, ids.ScreenId, DefaultUserId, cmd)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot add new line: %v", err)
|
|
}
|
|
prompt := []sstore.OpenAIPromptMessageType{{Role: sstore.OpenAIRoleUser, Content: promptStr}}
|
|
if resolveBool(pk.Kwargs["stream"], true) {
|
|
go doOpenAIStreamCompletion(cmd, opts, prompt)
|
|
} else {
|
|
go doOpenAICompletion(cmd, opts, prompt)
|
|
}
|
|
updateHistoryContext(ctx, line, cmd)
|
|
updateMap := make(map[string]interface{})
|
|
updateMap[sstore.ScreenField_SelectedLine] = line.LineNum
|
|
updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusInput
|
|
screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap)
|
|
if err != nil {
|
|
// ignore error again (nothing to do)
|
|
log.Printf("openai error updating screen selected line: %v\n", err)
|
|
}
|
|
update := &sstore.ModelUpdate{Line: line, Cmd: cmd, Screens: []*sstore.ScreenType{screen}}
|
|
return update, nil
|
|
}
|
|
|
|
func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/%s error: %w", GetCmdStr(pk), err)
|
|
}
|
|
newRemote := firstArg(pk)
|
|
if newRemote == "" {
|
|
return crShowCommand(ctx, pk, ids)
|
|
}
|
|
_, rptr, rstate, err := resolveRemote(ctx, newRemote, ids.SessionId, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rptr == nil {
|
|
return nil, fmt.Errorf("/%s error: remote %q not found", GetCmdStr(pk), newRemote)
|
|
}
|
|
if rstate.Archived {
|
|
return nil, fmt.Errorf("/%s error: remote %q cannot switch to archived remote", GetCmdStr(pk), newRemote)
|
|
}
|
|
err = sstore.UpdateCurRemote(ctx, ids.ScreenId, *rptr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/%s error: cannot update curremote: %w", GetCmdStr(pk), err)
|
|
}
|
|
noHist := resolveBool(pk.Kwargs["nohist"], false)
|
|
if noHist {
|
|
screen, err := sstore.GetScreenById(ctx, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/%s error: cannot resolve screen for update: %w", GetCmdStr(pk), err)
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Screens: []*sstore.ScreenType{screen},
|
|
Interactive: pk.Interactive,
|
|
}
|
|
return update, nil
|
|
}
|
|
outputStr := fmt.Sprintf("connected to %s", GetFullRemoteDisplayName(rptr, rstate))
|
|
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "", nil)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update.Interactive = pk.Interactive
|
|
return update, nil
|
|
}
|
|
|
|
func makeDynCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, termOpts sstore.TermOpts) (*sstore.CmdType, error) {
|
|
cmd := &sstore.CmdType{
|
|
ScreenId: ids.ScreenId,
|
|
LineId: scbase.GenWaveUUID(),
|
|
CmdStr: cmdStr,
|
|
RawCmdStr: cmdStr,
|
|
Remote: ids.Remote.RemotePtr,
|
|
TermOpts: termOpts,
|
|
Status: sstore.CmdStatusRunning,
|
|
RunOut: nil,
|
|
}
|
|
if ids.Remote.StatePtr != nil {
|
|
cmd.StatePtr = *ids.Remote.StatePtr
|
|
}
|
|
if ids.Remote.FeState != nil {
|
|
cmd.FeState = ids.Remote.FeState
|
|
}
|
|
err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, fmt.Errorf("cannot create local ptyout file for %s command: %w", metaCmd, err)
|
|
}
|
|
return cmd, nil
|
|
}
|
|
|
|
func makeStaticCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, cmdOutput []byte) (*sstore.CmdType, error) {
|
|
cmd := &sstore.CmdType{
|
|
ScreenId: ids.ScreenId,
|
|
LineId: scbase.GenWaveUUID(),
|
|
CmdStr: cmdStr,
|
|
RawCmdStr: cmdStr,
|
|
Remote: ids.Remote.RemotePtr,
|
|
TermOpts: sstore.TermOpts{Rows: shexec.DefaultTermRows, Cols: shexec.DefaultTermCols, FlexRows: true, MaxPtySize: remote.DefaultMaxPtySize},
|
|
Status: sstore.CmdStatusDone,
|
|
RunOut: nil,
|
|
}
|
|
if ids.Remote.StatePtr != nil {
|
|
cmd.StatePtr = *ids.Remote.StatePtr
|
|
}
|
|
if ids.Remote.FeState != nil {
|
|
cmd.FeState = ids.Remote.FeState
|
|
}
|
|
err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, fmt.Errorf("cannot create local ptyout file for %s command: %w", metaCmd, err)
|
|
}
|
|
// can ignore ptyupdate
|
|
_, err = sstore.AppendToCmdPtyBlob(ctx, ids.ScreenId, cmd.LineId, cmdOutput, 0)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, fmt.Errorf("cannot append to local ptyout file for %s command: %v", metaCmd, err)
|
|
}
|
|
return cmd, nil
|
|
}
|
|
|
|
func addLineForCmd(ctx context.Context, metaCmd string, shouldFocus bool, ids resolvedIds, cmd *sstore.CmdType, renderer string, lineState map[string]any) (*sstore.ModelUpdate, error) {
|
|
rtnLine, err := sstore.AddCmdLine(ctx, ids.ScreenId, DefaultUserId, cmd, renderer, lineState)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
screen, err := sstore.GetScreenById(ctx, ids.ScreenId)
|
|
if err != nil {
|
|
// ignore error here, because the command has already run (nothing to do)
|
|
log.Printf("%s error getting screen: %v\n", metaCmd, err)
|
|
}
|
|
if screen != nil {
|
|
updateMap := make(map[string]interface{})
|
|
updateMap[sstore.ScreenField_SelectedLine] = rtnLine.LineNum
|
|
if shouldFocus {
|
|
updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusCmd
|
|
}
|
|
screen, err = sstore.UpdateScreen(ctx, ids.ScreenId, updateMap)
|
|
if err != nil {
|
|
// ignore error again (nothing to do)
|
|
log.Printf("%s error updating screen selected line: %v\n", metaCmd, err)
|
|
}
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Line: rtnLine,
|
|
Cmd: cmd,
|
|
Screens: []*sstore.ScreenType{screen},
|
|
}
|
|
updateHistoryContext(ctx, rtnLine, cmd)
|
|
return update, nil
|
|
}
|
|
|
|
func updateHistoryContext(ctx context.Context, line *sstore.LineType, cmd *sstore.CmdType) {
|
|
ctxVal := ctx.Value(historyContextKey)
|
|
if ctxVal == nil {
|
|
return
|
|
}
|
|
hctx := ctxVal.(*historyContextType)
|
|
if line != nil {
|
|
hctx.LineId = line.LineId
|
|
hctx.LineNum = line.LineNum
|
|
}
|
|
if cmd != nil {
|
|
hctx.RemotePtr = &cmd.Remote
|
|
}
|
|
}
|
|
|
|
func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket {
|
|
sort.Slice(comps, func(i int, j int) bool {
|
|
c1 := comps[i]
|
|
c2 := comps[j]
|
|
c1mc := strings.HasPrefix(c1, "^")
|
|
c2mc := strings.HasPrefix(c2, "^")
|
|
if c1mc && !c2mc {
|
|
return true
|
|
}
|
|
if !c1mc && c2mc {
|
|
return false
|
|
}
|
|
return c1 < c2
|
|
})
|
|
if len(comps) == 0 {
|
|
comps = []string{"(no completions)"}
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: fmt.Sprintf("%s completions", compType),
|
|
InfoComps: comps,
|
|
InfoCompsMore: hasMore,
|
|
},
|
|
}
|
|
return update
|
|
}
|
|
|
|
func simpleCompCommandMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) {
|
|
if strings.HasPrefix(prefix, "/") {
|
|
compsCmd, _ := comp.DoSimpleComp(ctx, comp.CGTypeCommand, prefix, compCtx, nil)
|
|
compsMeta, _ := simpleCompMeta(ctx, prefix, compCtx, nil)
|
|
return comp.CombineCompReturn(comp.CGTypeCommandMeta, compsCmd, compsMeta), nil
|
|
} else {
|
|
compsCmd, _ := comp.DoSimpleComp(ctx, comp.CGTypeCommand, prefix, compCtx, nil)
|
|
compsBareCmd, _ := simpleCompBareCmds(ctx, prefix, compCtx, nil)
|
|
return comp.CombineCompReturn(comp.CGTypeCommand, compsCmd, compsBareCmd), nil
|
|
}
|
|
}
|
|
|
|
func simpleCompBareCmds(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) {
|
|
rtn := comp.CompReturn{}
|
|
for _, bmc := range BareMetaCmds {
|
|
if strings.HasPrefix(bmc.CmdStr, prefix) {
|
|
rtn.Entries = append(rtn.Entries, comp.CompEntry{Word: bmc.CmdStr, IsMetaCmd: true})
|
|
}
|
|
}
|
|
return &rtn, nil
|
|
}
|
|
|
|
func simpleCompMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) {
|
|
rtn := comp.CompReturn{}
|
|
validCommands := getValidCommands()
|
|
for _, cmd := range validCommands {
|
|
if strings.HasPrefix(cmd, "/_") && !strings.HasPrefix(prefix, "/_") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(cmd, prefix) {
|
|
rtn.Entries = append(rtn.Entries, comp.CompEntry{Word: cmd, IsMetaCmd: true})
|
|
}
|
|
}
|
|
return &rtn, nil
|
|
}
|
|
|
|
func doMetaCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix string, forDisplay bool) ([]string, bool, error) {
|
|
ids, err := resolveUiIds(ctx, pk, 0) // best effort
|
|
var comps []string
|
|
var hasMore bool
|
|
if ids.Remote != nil && ids.Remote.RState.IsConnected() {
|
|
comps, hasMore, err = doCompGen(ctx, pk, prefix, "file", forDisplay)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
validCommands := getValidCommands()
|
|
for _, cmd := range validCommands {
|
|
if strings.HasPrefix(cmd, prefix) {
|
|
if forDisplay {
|
|
comps = append(comps, "^"+cmd)
|
|
} else {
|
|
comps = append(comps, cmd)
|
|
}
|
|
}
|
|
}
|
|
return comps, hasMore, nil
|
|
}
|
|
|
|
func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix string, compType string, forDisplay bool) ([]string, bool, error) {
|
|
if compType == "metacommand" {
|
|
return doMetaCompGen(ctx, pk, prefix, forDisplay)
|
|
}
|
|
if !packet.IsValidCompGenType(compType) {
|
|
return nil, false, fmt.Errorf("/_compgen invalid type '%s'", compType)
|
|
}
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("/_compgen error: %w", err)
|
|
}
|
|
cgPacket := packet.MakeCompGenPacket()
|
|
cgPacket.ReqId = uuid.New().String()
|
|
cgPacket.CompType = compType
|
|
cgPacket.Prefix = prefix
|
|
cgPacket.Cwd = ids.Remote.FeState["cwd"]
|
|
resp, err := ids.Remote.MShell.PacketRpc(ctx, cgPacket)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if err = resp.Err(); err != nil {
|
|
return nil, false, err
|
|
}
|
|
comps := utilfn.GetStrArr(resp.Data, "comps")
|
|
hasMore := utilfn.GetBool(resp.Data, "hasmore")
|
|
return comps, hasMore, nil
|
|
}
|
|
|
|
func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, 0) // best-effort
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/_compgen error: %w", err)
|
|
}
|
|
cmdLine := firstArg(pk)
|
|
pos := len(cmdLine)
|
|
if pk.Kwargs["comppos"] != "" {
|
|
posArg, err := strconv.Atoi(pk.Kwargs["comppos"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/_compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err)
|
|
}
|
|
pos = posArg
|
|
}
|
|
if pos < 0 {
|
|
pos = 0
|
|
}
|
|
if pos > len(cmdLine) {
|
|
pos = len(cmdLine)
|
|
}
|
|
showComps := resolveBool(pk.Kwargs["compshow"], false)
|
|
cmdSP := utilfn.StrWithPos{Str: cmdLine, Pos: pos}
|
|
compCtx := comp.CompContext{}
|
|
if ids.Remote != nil {
|
|
rptr := ids.Remote.RemotePtr
|
|
compCtx.RemotePtr = &rptr
|
|
if ids.Remote.FeState != nil {
|
|
compCtx.Cwd = ids.Remote.FeState["cwd"]
|
|
}
|
|
}
|
|
compCtx.ForDisplay = showComps
|
|
crtn, newSP, err := comp.DoCompGen(ctx, cmdSP, compCtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if crtn == nil {
|
|
return nil, nil
|
|
}
|
|
if showComps {
|
|
compStrs := crtn.GetCompDisplayStrs()
|
|
return makeInfoFromComps(crtn.CompType, compStrs, crtn.HasMore), nil
|
|
}
|
|
if newSP == nil || cmdSP == *newSP {
|
|
return nil, nil
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
CmdLine: &sstore.CmdLineType{CmdLine: newSP.Str, CursorPos: newSP.Pos},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/comment error: %w", err)
|
|
}
|
|
text := firstArg(pk)
|
|
if strings.TrimSpace(text) == "" {
|
|
return nil, fmt.Errorf("cannot post empty comment")
|
|
}
|
|
rtnLine, err := sstore.AddCommentLine(ctx, ids.ScreenId, DefaultUserId, text)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updateHistoryContext(ctx, rtnLine, nil)
|
|
updateMap := make(map[string]interface{})
|
|
updateMap[sstore.ScreenField_SelectedLine] = rtnLine.LineNum
|
|
updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusInput
|
|
screen, err := sstore.UpdateScreen(ctx, ids.ScreenId, updateMap)
|
|
if err != nil {
|
|
// ignore error again (nothing to do)
|
|
log.Printf("/comment error updating screen selected line: %v\n", err)
|
|
}
|
|
update := &sstore.ModelUpdate{Line: rtnLine, Screens: []*sstore.ScreenType{screen}}
|
|
return update, nil
|
|
}
|
|
|
|
func maybeQuote(s string, quote bool) string {
|
|
if quote {
|
|
return fmt.Sprintf("%q", s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func mapToStrs(m map[string]bool) []string {
|
|
var rtn []string
|
|
for key, val := range m {
|
|
if val {
|
|
rtn = append(rtn, key)
|
|
}
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func formatStrs(strs []string, conj string, quote bool) string {
|
|
if len(strs) == 0 {
|
|
return "(none)"
|
|
}
|
|
if len(strs) == 1 {
|
|
return maybeQuote(strs[0], quote)
|
|
}
|
|
if len(strs) == 2 {
|
|
return fmt.Sprintf("%s %s %s", maybeQuote(strs[0], quote), conj, maybeQuote(strs[1], quote))
|
|
}
|
|
var buf bytes.Buffer
|
|
for idx := 0; idx < len(strs)-1; idx++ {
|
|
buf.WriteString(maybeQuote(strs[idx], quote))
|
|
buf.WriteString(", ")
|
|
}
|
|
buf.WriteString(conj)
|
|
buf.WriteString(" ")
|
|
buf.WriteString(maybeQuote(strs[len(strs)-1], quote))
|
|
return buf.String()
|
|
}
|
|
|
|
func validateName(name string, typeStr string) error {
|
|
if len(name) > MaxNameLen {
|
|
return fmt.Errorf("%s name too long, max length is %d", typeStr, MaxNameLen)
|
|
}
|
|
if !genericNameRe.MatchString(name) {
|
|
return fmt.Errorf("invalid %s name", typeStr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateShareName(name string) error {
|
|
if len(name) > MaxShareNameLen {
|
|
return fmt.Errorf("share name too long, max length is %d", MaxShareNameLen)
|
|
}
|
|
for _, ch := range name {
|
|
if !unicode.IsPrint(ch) {
|
|
return fmt.Errorf("invalid character %q in share name", string(ch))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRenderer(renderer string) error {
|
|
if renderer == "" {
|
|
return nil
|
|
}
|
|
if len(renderer) > MaxRendererLen {
|
|
return fmt.Errorf("renderer name too long, max length is %d", MaxRendererLen)
|
|
}
|
|
if !rendererRe.MatchString(renderer) {
|
|
return fmt.Errorf("invalid renderer format")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateColor(color string, typeStr string) error {
|
|
for _, c := range ColorNames {
|
|
if color == c {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(ColorNames, "or", false))
|
|
}
|
|
|
|
func validateRemoteColor(color string, typeStr string) error {
|
|
for _, c := range RemoteColorNames {
|
|
if color == c {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("invalid %s, valid colors are: %s", typeStr, formatStrs(RemoteColorNames, "or", false))
|
|
}
|
|
|
|
func SessionOpenSharedCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
activity := sstore.ActivityUpdate{ClickShared: 1}
|
|
err := sstore.UpdateCurrentActivity(ctx, activity)
|
|
if err != nil {
|
|
log.Printf("error updating click-shared: %v\n", err)
|
|
}
|
|
return nil, fmt.Errorf("shared sessions are not available in this version of prompt (stay tuned)")
|
|
}
|
|
|
|
func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
activate := resolveBool(pk.Kwargs["activate"], true)
|
|
newName := pk.Kwargs["name"]
|
|
if newName != "" {
|
|
err := validateName(newName, "session")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
update, err := sstore.InsertSessionWithName(ctx, newName, activate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func makeExternLink(urlStr string) string {
|
|
return fmt.Sprintf(`https://extern?%s`, url.QueryEscape(urlStr))
|
|
}
|
|
|
|
func ScreenWebShareCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
return nil, fmt.Errorf("websharing is no longer available")
|
|
}
|
|
|
|
func SessionDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, 0) // don't force R_Session
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sessionId := ""
|
|
if len(pk.Args) >= 1 {
|
|
ritem, err := resolveSession(ctx, pk.Args[0], ids.SessionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/session:purge error resolving session %q: %w", pk.Args[0], err)
|
|
}
|
|
if ritem == nil {
|
|
return nil, fmt.Errorf("/session:purge session %q not found", pk.Args[0])
|
|
}
|
|
sessionId = ritem.Id
|
|
} else {
|
|
sessionId = ids.SessionId
|
|
}
|
|
if sessionId == "" {
|
|
return nil, fmt.Errorf("/session:purge no sessionid found")
|
|
}
|
|
update, err := sstore.PurgeSession(ctx, sessionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot delete session: %v", err)
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func SessionArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, 0) // don't force R_Session
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sessionId := ""
|
|
if len(pk.Args) >= 1 {
|
|
ritem, err := resolveSession(ctx, pk.Args[0], ids.SessionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/session:archive error resolving session %q: %w", pk.Args[0], err)
|
|
}
|
|
if ritem == nil {
|
|
return nil, fmt.Errorf("/session:archive session %q not found", pk.Args[0])
|
|
}
|
|
sessionId = ritem.Id
|
|
} else {
|
|
sessionId = ids.SessionId
|
|
}
|
|
if sessionId == "" {
|
|
return nil, fmt.Errorf("/session:archive no sessionid found")
|
|
}
|
|
archiveVal := true
|
|
if len(pk.Args) >= 2 {
|
|
archiveVal = resolveBool(pk.Args[1], true)
|
|
}
|
|
if archiveVal {
|
|
update, err := sstore.ArchiveSession(ctx, sessionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot archive session: %v", err)
|
|
}
|
|
update.Info = &sstore.InfoMsgType{
|
|
InfoMsg: "session archived",
|
|
}
|
|
return update, nil
|
|
} else {
|
|
activate := resolveBool(pk.Kwargs["activate"], false)
|
|
update, err := sstore.UnArchiveSession(ctx, sessionId, activate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot un-archive session: %v", err)
|
|
}
|
|
update.Info = &sstore.InfoMsgType{
|
|
InfoMsg: "session un-archived",
|
|
}
|
|
return update, nil
|
|
}
|
|
}
|
|
|
|
func SessionShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
session, err := sstore.GetSessionById(ctx, ids.SessionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get session: %w", err)
|
|
}
|
|
if session == nil {
|
|
return nil, fmt.Errorf("session not found")
|
|
}
|
|
var buf bytes.Buffer
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "sessionid", session.SessionId))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "name", session.Name))
|
|
if session.SessionIdx != 0 {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "index", session.SessionIdx))
|
|
}
|
|
if session.Archived {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "archived", "true"))
|
|
ts := time.UnixMilli(session.ArchivedTs)
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "archivedts", ts.Format(TsFormatStr)))
|
|
}
|
|
stats, err := sstore.GetSessionStats(ctx, ids.SessionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting session stats: %w", err)
|
|
}
|
|
var screenArchiveStr string
|
|
if stats.NumArchivedScreens > 0 {
|
|
screenArchiveStr = fmt.Sprintf(" (%d archived)", stats.NumArchivedScreens)
|
|
}
|
|
buf.WriteString(fmt.Sprintf(" %-15s %d%s\n", "screens", stats.NumScreens, screenArchiveStr))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "lines", stats.NumLines))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "cmds", stats.NumCmds))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %0.2fM\n", "disksize", float64(stats.DiskStats.TotalSize)/1000000))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "disk-location", stats.DiskStats.Location))
|
|
return &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: "session info",
|
|
InfoLines: splitLinesForInfo(buf.String()),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func SessionShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
sessions, err := sstore.GetBareSessions(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error retrieving sessions: %v", err)
|
|
}
|
|
var buf bytes.Buffer
|
|
for _, session := range sessions {
|
|
var archivedStr string
|
|
if session.Archived {
|
|
archivedStr = " (archived)"
|
|
}
|
|
sessionIdxStr := "-"
|
|
if session.SessionIdx != 0 {
|
|
sessionIdxStr = strconv.Itoa(int(session.SessionIdx))
|
|
}
|
|
outStr := fmt.Sprintf("%-30s %s %s\n", session.Name+archivedStr, session.SessionId, sessionIdxStr)
|
|
buf.WriteString(outStr)
|
|
}
|
|
return &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: "all sessions",
|
|
InfoLines: splitLinesForInfo(buf.String()),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func SessionSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var varsUpdated []string
|
|
if pk.Kwargs["name"] != "" {
|
|
newName := pk.Kwargs["name"]
|
|
err = validateName(newName, "session")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = sstore.SetSessionName(ctx, ids.SessionId, newName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("setting session name: %v", err)
|
|
}
|
|
varsUpdated = append(varsUpdated, "name")
|
|
}
|
|
if pk.Kwargs["pos"] != "" {
|
|
|
|
}
|
|
if len(varsUpdated) == 0 {
|
|
return nil, fmt.Errorf("/session:set no updates, can set %s", formatStrs([]string{"name", "pos"}, "or", false))
|
|
}
|
|
bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId)
|
|
update := &sstore.ModelUpdate{
|
|
Sessions: []*sstore.SessionType{bareSession},
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("session updated %s", formatStrs(varsUpdated, "and", false)),
|
|
TimeoutMs: 2000,
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
firstArg := firstArg(pk)
|
|
if firstArg == "" {
|
|
return nil, fmt.Errorf("usage /session [name|id|pos], no param specified")
|
|
}
|
|
ritem, err := resolveSession(ctx, firstArg, ids.SessionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = sstore.SetActiveSessionId(ctx, ritem.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
ActiveSessionId: ritem.Id,
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("switched to session %q", ritem.Name),
|
|
TimeoutMs: 2000,
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
initPk, err := ids.Remote.MShell.ReInit(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if initPk == nil || initPk.State == nil {
|
|
return nil, fmt.Errorf("invalid initpk received from remote (no remote state)")
|
|
}
|
|
feState := sstore.FeStateFromShellState(initPk.State)
|
|
remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, feState, initPk.State, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputStr := "reset remote state"
|
|
cmd, err := makeStaticCmd(ctx, "reset", ids, pk.GetRawStr(), []byte(outputStr))
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update, err := addLineForCmd(ctx, "/reset", false, ids, cmd, "", nil)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update.Interactive = pk.Interactive
|
|
update.Sessions = sstore.MakeSessionsUpdateForRemote(ids.SessionId, remoteInst)
|
|
return update, nil
|
|
}
|
|
|
|
func ClearCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resolveBool(pk.Kwargs["purge"], false) {
|
|
update, err := sstore.PurgeScreenLines(ctx, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("clearing screen: %v", err)
|
|
}
|
|
update.Info = &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("screen cleared (all lines purged)"),
|
|
TimeoutMs: 2000,
|
|
}
|
|
return update, nil
|
|
} else {
|
|
update, err := sstore.ArchiveScreenLines(ctx, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("clearing screen: %v", err)
|
|
}
|
|
update.Info = &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("screen cleared"),
|
|
TimeoutMs: 2000,
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
}
|
|
|
|
func HistoryPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/history:purge requires at least one argument (history id)")
|
|
}
|
|
var historyIds []string
|
|
for _, historyArg := range pk.Args {
|
|
_, err := uuid.Parse(historyArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid historyid (must be uuid)")
|
|
}
|
|
historyIds = append(historyIds, historyArg)
|
|
}
|
|
historyItemsRemoved, err := sstore.PurgeHistoryByIds(ctx, historyIds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/history:purge error purging items: %v", err)
|
|
}
|
|
update := &sstore.ModelUpdate{}
|
|
for _, historyItem := range historyItemsRemoved {
|
|
if historyItem.LineId == "" {
|
|
continue
|
|
}
|
|
lineObj := &sstore.LineType{
|
|
ScreenId: historyItem.ScreenId,
|
|
LineId: historyItem.LineId,
|
|
Remove: true,
|
|
}
|
|
update.Lines = append(update.Lines, lineObj)
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
const HistoryViewPageSize = 50
|
|
|
|
var cmdFilterLs = regexp.MustCompile(`^ls(\s|$)`)
|
|
var cmdFilterCd = regexp.MustCompile(`^cd(\s|$)`)
|
|
|
|
func historyCmdFilter(hitem *sstore.HistoryItemType) bool {
|
|
cmdStr := hitem.CmdStr
|
|
if cmdStr == "" || strings.Index(cmdStr, ";") != -1 || strings.Index(cmdStr, "\n") != -1 {
|
|
return true
|
|
}
|
|
if cmdFilterLs.MatchString(cmdStr) {
|
|
return false
|
|
}
|
|
if cmdFilterCd.MatchString(cmdStr) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
_, err := resolveUiIds(ctx, pk, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
offset, err := resolveNonNegInt(pk.Kwargs["offset"], 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rawOffset, err := resolveNonNegInt(pk.Kwargs["rawoffset"], 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opts := sstore.HistoryQueryOpts{MaxItems: HistoryViewPageSize, Offset: offset, RawOffset: rawOffset}
|
|
if pk.Kwargs["text"] != "" {
|
|
opts.SearchText = pk.Kwargs["text"]
|
|
}
|
|
if pk.Kwargs["searchsession"] != "" {
|
|
sessionId, err := resolveSessionArg(pk.Kwargs["searchsession"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid searchsession: %v", err)
|
|
}
|
|
opts.SessionId = sessionId
|
|
}
|
|
if pk.Kwargs["searchremote"] != "" {
|
|
rptr, err := resolveRemoteArg(pk.Kwargs["searchremote"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid searchremote: %v", err)
|
|
}
|
|
if rptr != nil {
|
|
opts.RemoteId = rptr.RemoteId
|
|
}
|
|
}
|
|
if pk.Kwargs["fromts"] != "" {
|
|
fromTs, err := resolvePosInt(pk.Kwargs["fromts"], 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid fromts (must be unixtime (milliseconds): %v", err)
|
|
}
|
|
if fromTs > 0 {
|
|
opts.FromTs = int64(fromTs)
|
|
}
|
|
}
|
|
if pk.Kwargs["meta"] != "" {
|
|
opts.NoMeta = !resolveBool(pk.Kwargs["meta"], true)
|
|
}
|
|
if resolveBool(pk.Kwargs["filter"], false) {
|
|
opts.FilterFn = historyCmdFilter
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid meta arg (must be boolean): %v", err)
|
|
}
|
|
hresult, err := sstore.GetHistoryItems(ctx, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hvdata := &sstore.HistoryViewData{
|
|
Items: hresult.Items,
|
|
Offset: hresult.Offset,
|
|
RawOffset: hresult.RawOffset,
|
|
NextRawOffset: hresult.NextRawOffset,
|
|
HasMore: hresult.HasMore,
|
|
}
|
|
lines, cmds, err := sstore.GetLineCmdsFromHistoryItems(ctx, hvdata.Items)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hvdata.Lines = lines
|
|
hvdata.Cmds = cmds
|
|
update := &sstore.ModelUpdate{
|
|
HistoryViewData: hvdata,
|
|
MainView: sstore.MainViewHistory,
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
const DefaultMaxHistoryItems = 10000
|
|
|
|
func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
maxItems, err := resolvePosInt(pk.Kwargs["maxitems"], DefaultMaxHistoryItems)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid maxitems value '%s' (must be a number): %v", pk.Kwargs["maxitems"], err)
|
|
}
|
|
if maxItems < 0 {
|
|
return nil, fmt.Errorf("invalid maxitems value '%d' (cannot be negative)", maxItems)
|
|
}
|
|
if maxItems == 0 {
|
|
maxItems = DefaultMaxHistoryItems
|
|
}
|
|
htype := HistoryTypeScreen
|
|
hSessionId := ids.SessionId
|
|
hScreenId := ids.ScreenId
|
|
if pk.Kwargs["type"] != "" {
|
|
htype = pk.Kwargs["type"]
|
|
if htype != HistoryTypeScreen && htype != HistoryTypeSession && htype != HistoryTypeGlobal {
|
|
return nil, fmt.Errorf("invalid history type '%s', valid types: %s", htype, formatStrs([]string{HistoryTypeScreen, HistoryTypeSession, HistoryTypeGlobal}, "or", false))
|
|
}
|
|
}
|
|
if htype == HistoryTypeGlobal {
|
|
hSessionId = ""
|
|
hScreenId = ""
|
|
} else if htype == HistoryTypeSession {
|
|
hScreenId = ""
|
|
}
|
|
hopts := sstore.HistoryQueryOpts{MaxItems: maxItems, SessionId: hSessionId, ScreenId: hScreenId}
|
|
hresult, err := sstore.GetHistoryItems(ctx, hopts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
show := !resolveBool(pk.Kwargs["noshow"], false)
|
|
if show {
|
|
err = sstore.UpdateCurrentActivity(ctx, sstore.ActivityUpdate{HistoryView: 1})
|
|
if err != nil {
|
|
log.Printf("error updating current activity (history): %v\n", err)
|
|
}
|
|
}
|
|
update := &sstore.ModelUpdate{}
|
|
update.History = &sstore.HistoryInfoType{
|
|
HistoryType: htype,
|
|
SessionId: ids.SessionId,
|
|
ScreenId: ids.ScreenId,
|
|
Items: hresult.Items,
|
|
Show: show,
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func splitLinesForInfo(str string) []string {
|
|
rtn := strings.Split(str, "\n")
|
|
if rtn[len(rtn)-1] == "" {
|
|
return rtn[:len(rtn)-1]
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func resizeRunningCommand(ctx context.Context, cmd *sstore.CmdType, newCols int) error {
|
|
siPk := packet.MakeSpecialInputPacket()
|
|
siPk.CK = base.MakeCommandKey(cmd.ScreenId, cmd.LineId)
|
|
siPk.WinSize = &packet.WinSize{Rows: int(cmd.TermOpts.Rows), Cols: newCols}
|
|
msh := remote.GetRemoteById(cmd.Remote.RemoteId)
|
|
if msh == nil {
|
|
return fmt.Errorf("cannot resize, cmd remote not found")
|
|
}
|
|
err := msh.SendSpecialInput(siPk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newTermOpts := cmd.TermOpts
|
|
newTermOpts.Cols = int64(newCols)
|
|
err = sstore.UpdateCmdTermOpts(ctx, cmd.ScreenId, cmd.LineId, newTermOpts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ScreenResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
colsStr := pk.Kwargs["cols"]
|
|
if colsStr == "" {
|
|
return nil, fmt.Errorf("/screen:resize requires a numeric 'cols' argument")
|
|
}
|
|
cols, err := strconv.Atoi(colsStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:resize requires a numeric 'cols' argument: %v", err)
|
|
}
|
|
if cols <= 0 {
|
|
return nil, fmt.Errorf("/screen:resize invalid zero/negative 'cols' argument")
|
|
}
|
|
cols = base.BoundInt(cols, shexec.MinTermCols, shexec.MaxTermCols)
|
|
runningCmds, err := sstore.GetRunningScreenCmds(ctx, ids.ScreenId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/screen:resize cannot get running commands: %v", err)
|
|
}
|
|
if len(runningCmds) == 0 {
|
|
return nil, nil
|
|
}
|
|
for _, cmd := range runningCmds {
|
|
if int(cmd.TermOpts.Cols) != cols {
|
|
resizeRunningCommand(ctx, cmd, cols)
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func LineCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
return nil, fmt.Errorf("/line requires a subcommand: %s", formatStrs([]string{"show", "star", "hide", "purge", "setheight", "set"}, "or", false))
|
|
}
|
|
|
|
func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pk.Args) != 2 {
|
|
return nil, fmt.Errorf("/line:setheight requires 2 arguments (linearg and height)")
|
|
}
|
|
lineArg := pk.Args[0]
|
|
lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error looking up lineid: %v", err)
|
|
}
|
|
heightVal, err := resolveNonNegInt(pk.Args[1], 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:setheight invalid height val: %v", err)
|
|
}
|
|
if heightVal > 10000 {
|
|
return nil, fmt.Errorf("/line:setheight invalid height val (too large): %d", heightVal)
|
|
}
|
|
err = sstore.UpdateLineHeight(ctx, ids.ScreenId, lineId, heightVal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:setheight error updating height: %v", err)
|
|
}
|
|
// we don't need to pass the updated line height (it is "write only")
|
|
return nil, nil
|
|
}
|
|
|
|
func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pk.Args) != 1 {
|
|
return nil, fmt.Errorf("/line:set requires 1 argument (linearg)")
|
|
}
|
|
lineArg := pk.Args[0]
|
|
lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error looking up lineid: %v", err)
|
|
}
|
|
var varsUpdated []string
|
|
if renderer, found := pk.Kwargs[KwArgRenderer]; found {
|
|
if err = validateRenderer(renderer); err != nil {
|
|
return nil, fmt.Errorf("invalid renderer value: %w", err)
|
|
}
|
|
err = sstore.UpdateLineRenderer(ctx, ids.ScreenId, lineId, renderer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error changing line renderer: %v", err)
|
|
}
|
|
varsUpdated = append(varsUpdated, KwArgRenderer)
|
|
}
|
|
if view, found := pk.Kwargs[KwArgView]; found {
|
|
if err = validateRenderer(view); err != nil {
|
|
return nil, fmt.Errorf("invalid view value: %w", err)
|
|
}
|
|
err = sstore.UpdateLineRenderer(ctx, ids.ScreenId, lineId, view)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error changing line view: %v", err)
|
|
}
|
|
varsUpdated = append(varsUpdated, KwArgView)
|
|
}
|
|
if stateJson, found := pk.Kwargs[KwArgState]; found {
|
|
if len(stateJson) > sstore.MaxLineStateSize {
|
|
return nil, fmt.Errorf("invalid state value (too large), size[%d], max[%d]", len(stateJson), sstore.MaxLineStateSize)
|
|
}
|
|
var stateMap map[string]any
|
|
err = json.Unmarshal([]byte(stateJson), &stateMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid state value, cannot parse json: %v", err)
|
|
}
|
|
err = sstore.UpdateLineState(ctx, ids.ScreenId, lineId, stateMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot update linestate: %v", err)
|
|
}
|
|
varsUpdated = append(varsUpdated, KwArgState)
|
|
}
|
|
if len(varsUpdated) == 0 {
|
|
return nil, fmt.Errorf("/line:set requires a value to set: %s", formatStrs([]string{KwArgView, KwArgState}, "or", false))
|
|
}
|
|
updatedLine, err := sstore.GetLineById(ctx, ids.ScreenId, lineId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:set cannot retrieve updated line: %v", err)
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Line: updatedLine,
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("line updated %s", formatStrs(varsUpdated, "and", false)),
|
|
TimeoutMs: 2000,
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func LineViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) != 3 {
|
|
return nil, fmt.Errorf("usage /line:view [session] [screen] [line]")
|
|
}
|
|
sessionArg := pk.Args[0]
|
|
screenArg := pk.Args[1]
|
|
lineArg := pk.Args[2]
|
|
sessionId, err := resolveSessionArg(sessionArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:view invalid session arg: %v", err)
|
|
}
|
|
if sessionId == "" {
|
|
return nil, fmt.Errorf("/line:view no session found")
|
|
}
|
|
screenRItem, err := resolveSessionScreen(ctx, sessionId, screenArg, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:view invalid screen arg: %v", err)
|
|
}
|
|
if screenRItem == nil {
|
|
return nil, fmt.Errorf("/line:view no screen found")
|
|
}
|
|
screen, err := sstore.GetScreenById(ctx, screenRItem.Id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:view could not get screen: %v", err)
|
|
}
|
|
lineRItem, err := resolveLine(ctx, sessionId, screen.ScreenId, lineArg, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:view invalid line arg: %v", err)
|
|
}
|
|
update, err := sstore.SwitchScreenById(ctx, sessionId, screenRItem.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if lineRItem != nil {
|
|
updateMap := make(map[string]interface{})
|
|
updateMap[sstore.ScreenField_SelectedLine] = lineRItem.Num
|
|
updateMap[sstore.ScreenField_AnchorLine] = lineRItem.Num
|
|
updateMap[sstore.ScreenField_AnchorOffset] = 0
|
|
screen, err = sstore.UpdateScreen(ctx, screenRItem.Id, updateMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
update.Screens = []*sstore.ScreenType{screen}
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
// no resolve ui ids!
|
|
var tagName string // defaults to ''
|
|
if len(pk.Args) > 0 {
|
|
tagName = pk.Args[0]
|
|
}
|
|
bms, err := sstore.GetBookmarks(ctx, tagName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve bookmarks: %v", err)
|
|
}
|
|
err = sstore.UpdateCurrentActivity(ctx, sstore.ActivityUpdate{BookmarksView: 1})
|
|
if err != nil {
|
|
log.Printf("error updating current activity (bookmarks): %v\n", err)
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
MainView: sstore.MainViewBookmarks,
|
|
Bookmarks: bms,
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func BookmarkSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/bookmark:set requires one argument (bookmark id)")
|
|
}
|
|
bookmarkArg := pk.Args[0]
|
|
bookmarkId, err := sstore.GetBookmarkIdByArg(ctx, bookmarkArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error trying to resolve bookmark: %v", err)
|
|
}
|
|
if bookmarkId == "" {
|
|
return nil, fmt.Errorf("bookmark not found")
|
|
}
|
|
editMap := make(map[string]interface{})
|
|
if descStr, found := pk.Kwargs["desc"]; found {
|
|
editMap[sstore.BookmarkField_Desc] = descStr
|
|
}
|
|
if cmdStr, found := pk.Kwargs["cmdstr"]; found {
|
|
editMap[sstore.BookmarkField_CmdStr] = cmdStr
|
|
}
|
|
if len(editMap) == 0 {
|
|
return nil, fmt.Errorf("no fields set, can set %s", formatStrs([]string{"desc", "cmdstr"}, "or", false))
|
|
}
|
|
err = sstore.EditBookmark(ctx, bookmarkId, editMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error trying to edit bookmark: %v", err)
|
|
}
|
|
bm, err := sstore.GetBookmarkById(ctx, bookmarkId, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error retrieving edited bookmark: %v", err)
|
|
}
|
|
return &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: "bookmark edited",
|
|
},
|
|
Bookmarks: []*sstore.BookmarkType{bm},
|
|
}, nil
|
|
}
|
|
|
|
func BookmarkDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/bookmark:delete requires one argument (bookmark id)")
|
|
}
|
|
bookmarkArg := pk.Args[0]
|
|
bookmarkId, err := sstore.GetBookmarkIdByArg(ctx, bookmarkArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error trying to resolve bookmark: %v", err)
|
|
}
|
|
if bookmarkId == "" {
|
|
return nil, fmt.Errorf("bookmark not found")
|
|
}
|
|
err = sstore.DeleteBookmark(ctx, bookmarkId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error deleting bookmark: %v", err)
|
|
}
|
|
bm := &sstore.BookmarkType{BookmarkId: bookmarkId, Remove: true}
|
|
return &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: "bookmark deleted",
|
|
},
|
|
Bookmarks: []*sstore.BookmarkType{bm},
|
|
}, nil
|
|
}
|
|
|
|
func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/line:bookmark requires an argument (line number or id)")
|
|
}
|
|
lineArg := pk.Args[0]
|
|
lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error looking up lineid: %v", err)
|
|
}
|
|
if lineId == "" {
|
|
return nil, fmt.Errorf("line %q not found", lineArg)
|
|
}
|
|
_, cmdObj, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:bookmark error getting line: %v", err)
|
|
}
|
|
if cmdObj == nil {
|
|
return nil, fmt.Errorf("cannot bookmark non-cmd line")
|
|
}
|
|
existingBmIds, err := sstore.GetBookmarkIdsByCmdStr(ctx, cmdObj.CmdStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error trying to retrieve current boookmarks: %v", err)
|
|
}
|
|
var newBmId string
|
|
if len(existingBmIds) > 0 {
|
|
newBmId = existingBmIds[0]
|
|
} else {
|
|
newBm := &sstore.BookmarkType{
|
|
BookmarkId: uuid.New().String(),
|
|
CreatedTs: time.Now().UnixMilli(),
|
|
CmdStr: cmdObj.CmdStr,
|
|
Alias: "",
|
|
Tags: nil,
|
|
Description: "",
|
|
}
|
|
err = sstore.InsertBookmark(ctx, newBm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot insert bookmark: %v", err)
|
|
}
|
|
newBmId = newBm.BookmarkId
|
|
}
|
|
bms, err := sstore.GetBookmarks(ctx, "")
|
|
update := &sstore.ModelUpdate{
|
|
MainView: sstore.MainViewBookmarks,
|
|
Bookmarks: bms,
|
|
SelectedBookmark: newBmId,
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func LinePinCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/line:star requires an argument (line number or id)")
|
|
}
|
|
if len(pk.Args) > 2 {
|
|
return nil, fmt.Errorf("/line:star only takes up to 2 arguments (line-number and star-value)")
|
|
}
|
|
lineArg := pk.Args[0]
|
|
lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error looking up lineid: %v", err)
|
|
}
|
|
if lineId == "" {
|
|
return nil, fmt.Errorf("line %q not found", lineArg)
|
|
}
|
|
starVal, err := resolveNonNegInt(pk.Args[1], 1)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:star invalid star-value (not integer): %v", err)
|
|
}
|
|
if starVal > 5 {
|
|
return nil, fmt.Errorf("/line:star invalid star-value must be in the range of 0-5")
|
|
}
|
|
err = sstore.UpdateLineStar(ctx, ids.ScreenId, lineId, starVal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:star error updating star value: %v", err)
|
|
}
|
|
lineObj, err := sstore.GetLineById(ctx, ids.ScreenId, lineId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:star error getting line: %v", err)
|
|
}
|
|
if lineObj == nil {
|
|
// no line (which is strange given we checked for it above). just return a nop.
|
|
return nil, nil
|
|
}
|
|
return &sstore.ModelUpdate{Line: lineObj}, nil
|
|
}
|
|
|
|
func LineArchiveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/line:archive requires an argument (line number or id)")
|
|
}
|
|
lineArg := pk.Args[0]
|
|
lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error looking up lineid: %v", err)
|
|
}
|
|
if lineId == "" {
|
|
return nil, fmt.Errorf("line %q not found", lineArg)
|
|
}
|
|
shouldArchive := true
|
|
if len(pk.Args) >= 2 {
|
|
shouldArchive = resolveBool(pk.Args[1], true)
|
|
}
|
|
err = sstore.SetLineArchivedById(ctx, ids.ScreenId, lineId, shouldArchive)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:archive error updating hidden status: %v", err)
|
|
}
|
|
lineObj, err := sstore.GetLineById(ctx, ids.ScreenId, lineId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:archive error getting line: %v", err)
|
|
}
|
|
if lineObj == nil {
|
|
// no line (which is strange given we checked for it above). just return a nop.
|
|
return nil, nil
|
|
}
|
|
return &sstore.ModelUpdate{Line: lineObj}, nil
|
|
}
|
|
|
|
func LinePurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/line:purge requires at least one argument (line number or id)")
|
|
}
|
|
var lineIds []string
|
|
for _, lineArg := range pk.Args {
|
|
lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error looking up lineid: %v", err)
|
|
}
|
|
if lineId == "" {
|
|
return nil, fmt.Errorf("line %q not found", lineArg)
|
|
}
|
|
lineIds = append(lineIds, lineId)
|
|
}
|
|
err = sstore.PurgeLinesByIds(ctx, ids.ScreenId, lineIds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/line:purge error purging lines: %v", err)
|
|
}
|
|
update := &sstore.ModelUpdate{}
|
|
for _, lineId := range lineIds {
|
|
lineObj := &sstore.LineType{
|
|
ScreenId: ids.ScreenId,
|
|
LineId: lineId,
|
|
Remove: true,
|
|
}
|
|
update.Lines = append(update.Lines, lineObj)
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/line:show requires an argument (line number or id)")
|
|
}
|
|
lineArg := pk.Args[0]
|
|
lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error looking up lineid: %v", err)
|
|
}
|
|
if lineId == "" {
|
|
return nil, fmt.Errorf("line %q not found", lineArg)
|
|
}
|
|
line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting line: %v", err)
|
|
}
|
|
if line == nil {
|
|
return nil, fmt.Errorf("line %q not found", lineArg)
|
|
}
|
|
var buf bytes.Buffer
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "screenid", line.ScreenId))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "lineid", line.LineId))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "type", line.LineType))
|
|
lineNumStr := strconv.FormatInt(line.LineNum, 10)
|
|
if line.LineNumTemp {
|
|
lineNumStr = "~" + lineNumStr
|
|
}
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "linenum", lineNumStr))
|
|
ts := time.UnixMilli(line.Ts)
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "ts", ts.Format(TsFormatStr)))
|
|
if line.Ephemeral {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %v\n", "ephemeral", true))
|
|
}
|
|
if line.Renderer != "" {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "renderer", line.Renderer))
|
|
} else {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "renderer", "terminal"))
|
|
}
|
|
if cmd != nil {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "remote", cmd.Remote.MakeFullRemoteRef()))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "status", cmd.Status))
|
|
if cmd.FeState["cwd"] != "" {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "cwd", cmd.FeState["cwd"]))
|
|
}
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termopts", formatTermOpts(cmd.TermOpts)))
|
|
if cmd.TermOpts != cmd.OrigTermOpts {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "orig-termopts", formatTermOpts(cmd.OrigTermOpts)))
|
|
}
|
|
if cmd.RtnState {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "rtnstate", "true"))
|
|
}
|
|
stat, _ := sstore.StatCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId)
|
|
if stat == nil {
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", "-"))
|
|
} else {
|
|
fileDataStr := fmt.Sprintf("v%d data=%d offset=%d max=%s", stat.Version, stat.DataSize, stat.FileOffset, scbase.NumFormatB2(stat.MaxSize))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", stat.Location))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file-data", fileDataStr))
|
|
}
|
|
if cmd.DoneTs != 0 {
|
|
doneTs := time.UnixMilli(cmd.DoneTs)
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "donets", doneTs.Format(TsFormatStr)))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "exitcode", cmd.ExitCode))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %dms\n", "duration", cmd.DurationMs))
|
|
}
|
|
}
|
|
stateStr := dbutil.QuickJson(line.LineState)
|
|
if len(stateStr) > 80 {
|
|
stateStr = stateStr[0:77] + "..."
|
|
}
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "state", stateStr))
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: fmt.Sprintf("line %d info", line.LineNum),
|
|
InfoLines: splitLinesForInfo(buf.String()),
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func SetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
var setMap map[string]map[string]string
|
|
setMap = make(map[string]map[string]string)
|
|
_, err := resolveUiIds(ctx, pk, 0) // best effort
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for argIdx, rawArgVal := range pk.Args {
|
|
eqIdx := strings.Index(rawArgVal, "=")
|
|
if eqIdx == -1 {
|
|
return nil, fmt.Errorf("/set invalid argument %d, does not contain an '='", argIdx)
|
|
}
|
|
argName := rawArgVal[:eqIdx]
|
|
argVal := rawArgVal[eqIdx+1:]
|
|
ok, scopeName, varName := resolveSetArg(argName)
|
|
if !ok {
|
|
return nil, fmt.Errorf("/set invalid setvar %q", argName)
|
|
}
|
|
if _, ok := setMap[scopeName]; !ok {
|
|
setMap[scopeName] = make(map[string]string)
|
|
}
|
|
setMap[scopeName][varName] = argVal
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func makeStreamFilePk(ids resolvedIds, pk *scpacket.FeCommandPacketType) (*packet.StreamFilePacketType, error) {
|
|
cwd := ids.Remote.FeState["cwd"]
|
|
fileArg := pk.Args[0]
|
|
if fileArg == "" {
|
|
return nil, fmt.Errorf("/view:stat file argument must be set (cannot be empty)")
|
|
}
|
|
streamPk := packet.MakeStreamFilePacket()
|
|
streamPk.ReqId = uuid.New().String()
|
|
if filepath.IsAbs(fileArg) {
|
|
streamPk.Path = fileArg
|
|
} else {
|
|
streamPk.Path = filepath.Join(cwd, fileArg)
|
|
}
|
|
return streamPk, nil
|
|
}
|
|
|
|
func ViewStatCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/view:stat requires an argument (file name)")
|
|
}
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
streamPk, err := makeStreamFilePk(ids, pk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
streamPk.StatOnly = true
|
|
msh := ids.Remote.MShell
|
|
iter, err := msh.StreamFile(ctx, streamPk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/view:stat error: %v", err)
|
|
}
|
|
defer iter.Close()
|
|
respIf, err := iter.Next(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/view:stat error getting response: %v", err)
|
|
}
|
|
resp, ok := respIf.(*packet.StreamFileResponseType)
|
|
if !ok {
|
|
return nil, fmt.Errorf("/view:stat error, bad response packet type: %T", respIf)
|
|
}
|
|
if resp.Error != "" {
|
|
return nil, fmt.Errorf("/view:stat error: %s", resp.Error)
|
|
}
|
|
if resp.Info == nil {
|
|
return nil, fmt.Errorf("/view:stat error, no file info")
|
|
}
|
|
var buf bytes.Buffer
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "path", resp.Info.Name))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "size", resp.Info.Size))
|
|
modTs := time.UnixMilli(resp.Info.ModTs)
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "modts", modTs.Format(TsFormatStr)))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %v\n", "isdir", resp.Info.IsDir))
|
|
modeStr := fs.FileMode(resp.Info.Perm).String()
|
|
if len(modeStr) > 9 {
|
|
modeStr = modeStr[len(modeStr)-9:]
|
|
}
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "perms", modeStr))
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: fmt.Sprintf("view stat %q", streamPk.Path),
|
|
InfoLines: splitLinesForInfo(buf.String()),
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func ViewTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/view:test requires an argument (file name)")
|
|
}
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
streamPk, err := makeStreamFilePk(ids, pk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
msh := ids.Remote.MShell
|
|
iter, err := msh.StreamFile(ctx, streamPk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/view:test error: %v", err)
|
|
}
|
|
defer iter.Close()
|
|
respIf, err := iter.Next(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/view:test error getting response: %v", err)
|
|
}
|
|
resp, ok := respIf.(*packet.StreamFileResponseType)
|
|
if !ok {
|
|
return nil, fmt.Errorf("/view:test error, bad response packet type: %T", respIf)
|
|
}
|
|
if resp.Error != "" {
|
|
return nil, fmt.Errorf("/view:test error: %s", resp.Error)
|
|
}
|
|
if resp.Info == nil {
|
|
return nil, fmt.Errorf("/view:test error, no file info")
|
|
}
|
|
var buf bytes.Buffer
|
|
var numPackets int
|
|
for {
|
|
dataPkIf, err := iter.Next(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/view:test error while getting data: %w", err)
|
|
}
|
|
if dataPkIf == nil {
|
|
break
|
|
}
|
|
dataPk, ok := dataPkIf.(*packet.FileDataPacketType)
|
|
if !ok {
|
|
return nil, fmt.Errorf("/view:test invalid data packet type: %T", dataPkIf)
|
|
}
|
|
if dataPk.Error != "" {
|
|
return nil, fmt.Errorf("/view:test error returned while getting data: %s", dataPk.Error)
|
|
}
|
|
numPackets++
|
|
buf.Write(dataPk.Data)
|
|
}
|
|
buf.WriteString(fmt.Sprintf("\n\ntotal packets: %d\n", numPackets))
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: fmt.Sprintf("view file %q", streamPk.Path),
|
|
InfoLines: splitLinesForInfo(buf.String()),
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func CodeEditCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
|
|
}
|
|
// TODO more error checking on filename format?
|
|
if pk.Args[0] == "" {
|
|
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
|
|
}
|
|
langArg, err := getLangArg(pk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s invalid 'lang': %v", GetCmdStr(pk), err)
|
|
}
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0])
|
|
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
// set the line state
|
|
lineState := make(map[string]any)
|
|
lineState[sstore.LineState_Source] = "file"
|
|
lineState[sstore.LineState_File] = pk.Args[0]
|
|
if GetCmdStr(pk) == "codeview" {
|
|
lineState[sstore.LineState_Mode] = "view"
|
|
} else {
|
|
lineState[sstore.LineState_Mode] = "edit"
|
|
}
|
|
if langArg != "" {
|
|
lineState[sstore.LineState_Lang] = langArg
|
|
}
|
|
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), true, ids, cmd, "code", lineState)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update.Interactive = pk.Interactive
|
|
return update, nil
|
|
}
|
|
|
|
func CSVViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
|
|
}
|
|
// TODO more error checking on filename format?
|
|
if pk.Args[0] == "" {
|
|
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
|
|
}
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0])
|
|
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
// set the line state
|
|
lineState := make(map[string]any)
|
|
lineState[sstore.LineState_Source] = "file"
|
|
lineState[sstore.LineState_File] = pk.Args[0]
|
|
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), true, ids, cmd, "csv", lineState)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update.Interactive = pk.Interactive
|
|
return update, nil
|
|
}
|
|
|
|
func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
|
|
}
|
|
// TODO more error checking on filename format?
|
|
if pk.Args[0] == "" {
|
|
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
|
|
}
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0])
|
|
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
// set the line state
|
|
lineState := make(map[string]any)
|
|
lineState[sstore.LineState_Source] = "file"
|
|
lineState[sstore.LineState_File] = pk.Args[0]
|
|
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "image", lineState)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update.Interactive = pk.Interactive
|
|
return update, nil
|
|
}
|
|
|
|
func MarkdownViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
|
|
}
|
|
// TODO more error checking on filename format?
|
|
if pk.Args[0] == "" {
|
|
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
|
|
}
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0])
|
|
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
// set the line state
|
|
lineState := make(map[string]any)
|
|
lineState[sstore.LineState_Source] = "file"
|
|
lineState[sstore.LineState_File] = pk.Args[0]
|
|
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "markdown", lineState)
|
|
if err != nil {
|
|
// TODO tricky error since the command was a success, but we can't show the output
|
|
return nil, err
|
|
}
|
|
update.Interactive = pk.Interactive
|
|
return update, nil
|
|
}
|
|
|
|
func EditTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/edit:test requires an argument (file name)")
|
|
}
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
content, ok := pk.Kwargs["content"]
|
|
if !ok {
|
|
return nil, fmt.Errorf("/edit:test no content for file specified")
|
|
}
|
|
fileArg := pk.Args[0]
|
|
if fileArg == "" {
|
|
return nil, fmt.Errorf("/view:stat file argument must be set (cannot be empty)")
|
|
}
|
|
writePk := packet.MakeWriteFilePacket()
|
|
writePk.ReqId = uuid.New().String()
|
|
writePk.UseTemp = true
|
|
cwd := ids.Remote.FeState["cwd"]
|
|
if filepath.IsAbs(fileArg) {
|
|
writePk.Path = fileArg
|
|
} else {
|
|
writePk.Path = filepath.Join(cwd, fileArg)
|
|
}
|
|
msh := ids.Remote.MShell
|
|
iter, err := msh.PacketRpcIter(ctx, writePk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/edit:test error: %v", err)
|
|
}
|
|
// first packet should be WriteFileReady
|
|
readyIf, err := iter.Next(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/edit:test error while getting ready response: %w", err)
|
|
}
|
|
readyPk, ok := readyIf.(*packet.WriteFileReadyPacketType)
|
|
if !ok {
|
|
return nil, fmt.Errorf("/edit:test bad ready packet received: %T", readyIf)
|
|
}
|
|
if readyPk.Error != "" {
|
|
return nil, fmt.Errorf("/edit:test %s", readyPk.Error)
|
|
}
|
|
dataPk := packet.MakeFileDataPacket(writePk.ReqId)
|
|
dataPk.Data = []byte(content)
|
|
dataPk.Eof = true
|
|
err = msh.SendFileData(dataPk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/edit:test error sending data packet: %v", err)
|
|
}
|
|
doneIf, err := iter.Next(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("/edit:test error while getting done response: %w", err)
|
|
}
|
|
donePk, ok := doneIf.(*packet.WriteFileDonePacketType)
|
|
if !ok {
|
|
return nil, fmt.Errorf("/edit:test bad done packet received: %T", doneIf)
|
|
}
|
|
if donePk.Error != "" {
|
|
return nil, fmt.Errorf("/edit:test %s", donePk.Error)
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: fmt.Sprintf("edit test, wrote %q", writePk.Path),
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func SignalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pk.Args) == 0 {
|
|
return nil, fmt.Errorf("/signal requires a first argument (line number or id)")
|
|
}
|
|
if len(pk.Args) == 1 {
|
|
return nil, fmt.Errorf("/signal requires a second argument (signal name)")
|
|
}
|
|
lineArg := pk.Args[0]
|
|
lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error looking up lineid: %v", err)
|
|
}
|
|
line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting line: %v", err)
|
|
}
|
|
if line == nil {
|
|
return nil, fmt.Errorf("line %q not found", lineArg)
|
|
}
|
|
if cmd == nil {
|
|
return nil, fmt.Errorf("line %q does not have a command", lineArg)
|
|
}
|
|
if cmd.Status != sstore.CmdStatusRunning {
|
|
return nil, fmt.Errorf("line %q command is not running, cannot send signal", lineArg)
|
|
}
|
|
sigArg := pk.Args[1]
|
|
if isAllDigits(sigArg) {
|
|
val, _ := strconv.Atoi(sigArg)
|
|
if val <= 0 || val > MaxSignalNum {
|
|
return nil, fmt.Errorf("signal number is out of bounds: %q", sigArg)
|
|
}
|
|
} else if !strings.HasPrefix(sigArg, "SIG") {
|
|
sigArg = "SIG" + sigArg
|
|
}
|
|
sigArg = strings.ToUpper(sigArg)
|
|
if len(sigArg) > 12 {
|
|
return nil, fmt.Errorf("invalid signal (too long): %q", sigArg)
|
|
}
|
|
if !sigNameRe.MatchString(sigArg) {
|
|
return nil, fmt.Errorf("invalid signal name/number: %q", sigArg)
|
|
}
|
|
msh := remote.GetRemoteById(cmd.Remote.RemoteId)
|
|
if msh == nil {
|
|
return nil, fmt.Errorf("cannot send signal, no remote found for command")
|
|
}
|
|
if !msh.IsConnected() {
|
|
return nil, fmt.Errorf("cannot send signal, remote is not connected")
|
|
}
|
|
siPk := packet.MakeSpecialInputPacket()
|
|
siPk.CK = base.MakeCommandKey(cmd.ScreenId, cmd.LineId)
|
|
siPk.SigName = sigArg
|
|
err = msh.SendSpecialInput(siPk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot send signal: %v", err)
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("sent line %s signal %s", lineArg, sigArg),
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
go func() {
|
|
log.Printf("received /killserver, shutting down\n")
|
|
time.Sleep(1 * time.Second)
|
|
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
|
|
}()
|
|
return nil, nil
|
|
}
|
|
|
|
func ClientCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"show", "set"}, "or", false))
|
|
}
|
|
|
|
func ClientNotifyUpdateWriterCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
pcloud.ResetUpdateWriterNumFailures()
|
|
sstore.NotifyUpdateWriter()
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("notified update writer"),
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func boolToStr(v bool, trueStr string, falseStr string) string {
|
|
if v {
|
|
return trueStr
|
|
}
|
|
return falseStr
|
|
}
|
|
|
|
func ClientAcceptTosCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
clientOpts := clientData.ClientOpts
|
|
clientOpts.AcceptedTos = time.Now().UnixMilli()
|
|
err = sstore.SetClientOpts(ctx, clientOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating client data: %v", err)
|
|
}
|
|
clientData, err = sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
ClientData: clientData,
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func validateOpenAIAPIToken(key string) error {
|
|
if len(key) == 0 {
|
|
return fmt.Errorf("invalid openai token, zero length")
|
|
}
|
|
if len(key) > MaxOpenAIAPITokenLen {
|
|
return fmt.Errorf("invalid openai token, too long")
|
|
}
|
|
for idx, ch := range key {
|
|
if !unicode.IsPrint(ch) {
|
|
return fmt.Errorf("invalid openai token, char at idx:%d is invalid %q", idx, string(ch))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateOpenAIModel(model string) error {
|
|
if len(model) == 0 {
|
|
return nil
|
|
}
|
|
if len(model) > MaxOpenAIModelLen {
|
|
return fmt.Errorf("invalid openai model, too long")
|
|
}
|
|
for idx, ch := range model {
|
|
if !unicode.IsPrint(ch) {
|
|
return fmt.Errorf("invalid openai model, char at idx:%d is invalid %q", idx, string(ch))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
var varsUpdated []string
|
|
if fontSizeStr, found := pk.Kwargs["termfontsize"]; found {
|
|
newFontSize, err := resolveNonNegInt(fontSizeStr, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid termfontsize, must be a number between 8-15: %v", err)
|
|
}
|
|
if newFontSize < TermFontSizeMin || newFontSize > TermFontSizeMax {
|
|
return nil, fmt.Errorf("invalid termfontsize, must be a number between %d-%d", TermFontSizeMin, TermFontSizeMax)
|
|
}
|
|
feOpts := clientData.FeOpts
|
|
feOpts.TermFontSize = newFontSize
|
|
err = sstore.UpdateClientFeOpts(ctx, feOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating client feopts: %v", err)
|
|
}
|
|
varsUpdated = append(varsUpdated, "termfontsize")
|
|
}
|
|
if apiToken, found := pk.Kwargs["openaiapitoken"]; found {
|
|
err = validateOpenAIAPIToken(apiToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
varsUpdated = append(varsUpdated, "openaiapitoken")
|
|
aiOpts := clientData.OpenAIOpts
|
|
if aiOpts == nil {
|
|
aiOpts = &sstore.OpenAIOptsType{}
|
|
clientData.OpenAIOpts = aiOpts
|
|
}
|
|
aiOpts.APIToken = apiToken
|
|
err = sstore.UpdateClientOpenAIOpts(ctx, *aiOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating client openai api token: %v", err)
|
|
}
|
|
}
|
|
if aiModel, found := pk.Kwargs["openaimodel"]; found {
|
|
err = validateOpenAIModel(aiModel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
varsUpdated = append(varsUpdated, "openaimodel")
|
|
aiOpts := clientData.OpenAIOpts
|
|
if aiOpts == nil {
|
|
aiOpts = &sstore.OpenAIOptsType{}
|
|
clientData.OpenAIOpts = aiOpts
|
|
}
|
|
aiOpts.Model = aiModel
|
|
err = sstore.UpdateClientOpenAIOpts(ctx, *aiOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating client openai model: %v", err)
|
|
}
|
|
}
|
|
if maxTokensStr, found := pk.Kwargs["openaimaxtokens"]; found {
|
|
maxTokens, err := strconv.Atoi(maxTokensStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating client openai maxtokens, invalid number: %v", err)
|
|
}
|
|
if maxTokens < 0 || maxTokens > 1000000 {
|
|
return nil, fmt.Errorf("error updating client openai maxtokens, out of range: %d", maxTokens)
|
|
}
|
|
varsUpdated = append(varsUpdated, "openaimaxtokens")
|
|
aiOpts := clientData.OpenAIOpts
|
|
if aiOpts == nil {
|
|
aiOpts = &sstore.OpenAIOptsType{}
|
|
clientData.OpenAIOpts = aiOpts
|
|
}
|
|
aiOpts.MaxTokens = maxTokens
|
|
err = sstore.UpdateClientOpenAIOpts(ctx, *aiOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating client openai maxtokens: %v", err)
|
|
}
|
|
}
|
|
if maxChoicesStr, found := pk.Kwargs["openaimaxchoices"]; found {
|
|
maxChoices, err := strconv.Atoi(maxChoicesStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating client openai maxchoices, invalid number: %v", err)
|
|
}
|
|
if maxChoices < 0 || maxChoices > 10 {
|
|
return nil, fmt.Errorf("error updating client openai maxchoices, out of range: %d", maxChoices)
|
|
}
|
|
varsUpdated = append(varsUpdated, "openaimaxchoices")
|
|
aiOpts := clientData.OpenAIOpts
|
|
if aiOpts == nil {
|
|
aiOpts = &sstore.OpenAIOptsType{}
|
|
clientData.OpenAIOpts = aiOpts
|
|
}
|
|
aiOpts.MaxChoices = maxChoices
|
|
err = sstore.UpdateClientOpenAIOpts(ctx, *aiOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating client openai maxchoices: %v", err)
|
|
}
|
|
}
|
|
if aiBaseURL, found := pk.Kwargs["openaibaseurl"]; found {
|
|
aiOpts := clientData.OpenAIOpts
|
|
if aiOpts == nil {
|
|
aiOpts = &sstore.OpenAIOptsType{}
|
|
clientData.OpenAIOpts = aiOpts
|
|
}
|
|
aiOpts.BaseURL = aiBaseURL
|
|
varsUpdated = append(varsUpdated, "openaibaseurl")
|
|
err = sstore.UpdateClientOpenAIOpts(ctx, *aiOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error updating client openai base url: %v", err)
|
|
}
|
|
}
|
|
if len(varsUpdated) == 0 {
|
|
return nil, fmt.Errorf("/client:set requires a value to set: %s", formatStrs([]string{"termfontsize", "openaiapitoken", "openaimodel", "openaibaseurl", "openaimaxtokens", "openaimaxchoices"}, "or", false))
|
|
}
|
|
clientData, err = sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
|
}
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoMsg: fmt.Sprintf("client updated %s", formatStrs(varsUpdated, "and", false)),
|
|
TimeoutMs: 2000,
|
|
},
|
|
ClientData: clientData,
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
dbVersion, err := sstore.GetDBVersion(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve db version: %v\n", err)
|
|
}
|
|
clientVersion := "-"
|
|
if pk.UIContext != nil && pk.UIContext.Build != "" {
|
|
clientVersion = pk.UIContext.Build
|
|
}
|
|
var buf bytes.Buffer
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on")))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "release-check", boolToStr(clientData.ClientOpts.NoReleaseCheck, "off", "on")))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "db-version", dbVersion))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "client-version", clientVersion))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s %s\n", "server-version", scbase.WaveVersion, scbase.BuildTime))
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s (%s)\n", "arch", scbase.ClientArch(), scbase.MacOSRelease()))
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: fmt.Sprintf("client info"),
|
|
InfoLines: splitLinesForInfo(buf.String()),
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func TelemetryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
return nil, fmt.Errorf("/telemetry requires a subcommand: %s", formatStrs([]string{"show", "on", "off", "send"}, "or", false))
|
|
}
|
|
|
|
func setNoTelemetry(ctx context.Context, clientData *sstore.ClientData, noTelemetryVal bool) error {
|
|
clientOpts := clientData.ClientOpts
|
|
clientOpts.NoTelemetry = noTelemetryVal
|
|
err := sstore.SetClientOpts(ctx, clientOpts)
|
|
if err != nil {
|
|
return fmt.Errorf("error trying to update client telemetry: %v", err)
|
|
}
|
|
log.Printf("client no-telemetry setting updated to %v\n", noTelemetryVal)
|
|
go func() {
|
|
err := pcloud.SendNoTelemetryUpdate(ctx, clientOpts.NoTelemetry)
|
|
if err != nil {
|
|
log.Printf("[error] sending no-telemetry update: %v\n", err)
|
|
log.Printf("note that telemetry update has still taken effect locally, and will be respected by the client\n")
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func TelemetryOnCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
if !clientData.ClientOpts.NoTelemetry {
|
|
return sstore.InfoMsgUpdate("telemetry is already on"), nil
|
|
}
|
|
err = setNoTelemetry(ctx, clientData, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
go func() {
|
|
err := pcloud.SendTelemetry(ctx, false)
|
|
if err != nil {
|
|
// ignore error, but log
|
|
log.Printf("[error] sending telemetry update (in /telemetry:on): %v\n", err)
|
|
}
|
|
}()
|
|
clientData, err = sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
|
}
|
|
update := sstore.InfoMsgUpdate("telemetry is now on")
|
|
update.ClientData = clientData
|
|
return update, nil
|
|
}
|
|
|
|
func TelemetryOffCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
if clientData.ClientOpts.NoTelemetry {
|
|
return sstore.InfoMsgUpdate("telemetry is already off"), nil
|
|
}
|
|
err = setNoTelemetry(ctx, clientData, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientData, err = sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
|
}
|
|
update := sstore.InfoMsgUpdate("telemetry is now off")
|
|
update.ClientData = clientData
|
|
return update, nil
|
|
}
|
|
|
|
func TelemetryShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
var buf bytes.Buffer
|
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on")))
|
|
update := &sstore.ModelUpdate{
|
|
Info: &sstore.InfoMsgType{
|
|
InfoTitle: fmt.Sprintf("telemetry info"),
|
|
InfoLines: splitLinesForInfo(buf.String()),
|
|
},
|
|
}
|
|
return update, nil
|
|
}
|
|
|
|
func TelemetrySendCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
force := resolveBool(pk.Kwargs["force"], false)
|
|
if clientData.ClientOpts.NoTelemetry && !force {
|
|
return nil, fmt.Errorf("cannot send telemetry, telemetry is off. pass force=1 to force the send, or turn on telemetry with /telemetry:on")
|
|
}
|
|
err = pcloud.SendTelemetry(ctx, force)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send telemetry: %v", err)
|
|
}
|
|
return sstore.InfoMsgUpdate("telemetry sent"), nil
|
|
}
|
|
|
|
func runReleaseCheck(ctx context.Context, force bool) error {
|
|
rslt, err := releasechecker.CheckNewRelease(ctx, force)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("error checking for new release: %v", err)
|
|
}
|
|
|
|
if rslt == releasechecker.Failure {
|
|
return fmt.Errorf("error checking for new release, see log for details")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func setNoReleaseCheck(ctx context.Context, clientData *sstore.ClientData, noReleaseCheckValue bool) error {
|
|
clientOpts := clientData.ClientOpts
|
|
clientOpts.NoReleaseCheck = noReleaseCheckValue
|
|
err := sstore.SetClientOpts(ctx, clientOpts)
|
|
if err != nil {
|
|
return fmt.Errorf("error trying to update client releaseCheck setting: %v", err)
|
|
}
|
|
log.Printf("client no-release-check setting updated to %v\n", noReleaseCheckValue)
|
|
return nil
|
|
}
|
|
|
|
func ReleaseCheckOnCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
if !clientData.ClientOpts.NoReleaseCheck {
|
|
return sstore.InfoMsgUpdate("release check is already on"), nil
|
|
}
|
|
err = setNoReleaseCheck(ctx, clientData, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = runReleaseCheck(ctx, true)
|
|
if err != nil {
|
|
log.Printf("error checking for new release after enabling auto release check: %v\n", err)
|
|
}
|
|
|
|
clientData, err = sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
|
}
|
|
update := sstore.InfoMsgUpdate("automatic release checking is now on")
|
|
update.ClientData = clientData
|
|
return update, nil
|
|
}
|
|
|
|
func ReleaseCheckOffCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
|
}
|
|
if clientData.ClientOpts.NoReleaseCheck {
|
|
return sstore.InfoMsgUpdate("release check is already off"), nil
|
|
}
|
|
err = setNoReleaseCheck(ctx, clientData, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientData, err = sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
|
}
|
|
update := sstore.InfoMsgUpdate("automatic release checking is now off")
|
|
update.ClientData = clientData
|
|
return update, nil
|
|
}
|
|
|
|
func ReleaseCheckCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
|
err := runReleaseCheck(ctx, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clientData, err := sstore.EnsureClientData(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
|
}
|
|
|
|
var rsp string
|
|
if semver.Compare(scbase.WaveVersion, clientData.ReleaseInfo.LatestVersion) < 0 {
|
|
rsp = "new release available to download: https://www.waveterm.dev/download"
|
|
} else {
|
|
rsp = "no new release available"
|
|
}
|
|
|
|
update := sstore.InfoMsgUpdate(rsp)
|
|
update.ClientData = clientData
|
|
return update, nil
|
|
}
|
|
|
|
func formatTermOpts(termOpts sstore.TermOpts) string {
|
|
if termOpts.Cols == 0 {
|
|
return "???"
|
|
}
|
|
rtnStr := fmt.Sprintf("%dx%d", termOpts.Rows, termOpts.Cols)
|
|
if termOpts.FlexRows {
|
|
rtnStr += " flexrows"
|
|
}
|
|
if termOpts.MaxPtySize > 0 {
|
|
rtnStr += " maxbuf=" + scbase.NumFormatB2(termOpts.MaxPtySize)
|
|
}
|
|
return rtnStr
|
|
}
|
|
|
|
type ColMeta struct {
|
|
Title string
|
|
MinCols int
|
|
MaxCols int
|
|
}
|
|
|
|
func toInterfaceArr(sarr []string) []interface{} {
|
|
rtn := make([]interface{}, len(sarr))
|
|
for idx, s := range sarr {
|
|
rtn[idx] = s
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func formatTextTable(totalCols int, data [][]string, colMeta []ColMeta) []string {
|
|
numCols := len(colMeta)
|
|
maxColLen := make([]int, len(colMeta))
|
|
for i, cm := range colMeta {
|
|
maxColLen[i] = cm.MinCols
|
|
}
|
|
for _, row := range data {
|
|
for i := 0; i < numCols && i < len(row); i++ {
|
|
dlen := len(row[i])
|
|
if dlen > maxColLen[i] {
|
|
maxColLen[i] = dlen
|
|
}
|
|
}
|
|
}
|
|
fmtStr := ""
|
|
for idx, clen := range maxColLen {
|
|
if idx != 0 {
|
|
fmtStr += " "
|
|
}
|
|
fmtStr += fmt.Sprintf("%%%ds", clen)
|
|
}
|
|
var rtn []string
|
|
for _, row := range data {
|
|
sval := fmt.Sprintf(fmtStr, toInterfaceArr(row)...)
|
|
rtn = append(rtn, sval)
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func isValidInScope(scopeName string, varName string) bool {
|
|
for _, varScope := range SetVarScopes {
|
|
if varScope.ScopeName == scopeName {
|
|
return utilfn.ContainsStr(varScope.VarNames, varName)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// returns (is-valid, scope, name)
|
|
// TODO write a full resolver to allow for indexed arguments. e.g. session[1].screen[1].screen.pterm="25x80"
|
|
func resolveSetArg(argName string) (bool, string, string) {
|
|
dotIdx := strings.Index(argName, ".")
|
|
if dotIdx == -1 {
|
|
argName = SetVarNameMap[argName]
|
|
dotIdx = strings.Index(argName, ".")
|
|
}
|
|
if argName == "" {
|
|
return false, "", ""
|
|
}
|
|
scopeName := argName[0:dotIdx]
|
|
varName := argName[dotIdx+1:]
|
|
if !isValidInScope(scopeName, varName) {
|
|
return false, "", ""
|
|
}
|
|
return true, scopeName, varName
|
|
}
|