feat: create backend for user input requests

This is the first part of a change that allows the backend to request
user input from the frontend. Essentially, the backend will send a
request for the user to answer some query, and the frontend will send
that answer back. It is blocking, so it needs to be used within a
goroutine.

There is some placeholder code in the frontend that will be updated in
future commits. Similarly, there is some debug code in the backend
remote.go file.
This commit is contained in:
Sylvia Crowe 2024-01-29 16:19:34 -08:00
parent 1a51d93a54
commit 98f6b84d3e
7 changed files with 159 additions and 30 deletions

View File

@ -50,6 +50,9 @@ import {
HistoryViewDataType, HistoryViewDataType,
AlertMessageType, AlertMessageType,
HistorySearchParams, HistorySearchParams,
UserInputRequest,
UserInputResponse,
UserInputResponsePacket,
FocusTypeStrs, FocusTypeStrs,
ScreenLinesType, ScreenLinesType,
HistoryTypeStrs, HistoryTypeStrs,
@ -4110,6 +4113,19 @@ class Model {
update.screennumrunningcommands.num update.screennumrunningcommands.num
); );
} }
if ("userinputrequest" in update) {
let userInputRequest: UserInputRequest = update.userinputrequest;
let userInputResponse: UserInputResponse = {
type: "text",
text: "what wonderful weather we're having",
};
let userInputResponsePacket: UserInputResponsePacket = {
type: "userinputresp",
requestid: userInputRequest.requestid,
response: userInputResponse,
};
this.ws.pushMessage(userInputResponsePacket);
}
} }
updateRemotes(remotes: RemoteType[]): void { updateRemotes(remotes: RemoteType[]): void {

View File

@ -328,6 +328,7 @@ type ModelUpdateType = {
alertmessage?: AlertMessageType; alertmessage?: AlertMessageType;
screenstatusindicator?: ScreenStatusIndicatorUpdateType; screenstatusindicator?: ScreenStatusIndicatorUpdateType;
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType; screennumrunningcommands?: ScreenNumRunningCommandsUpdateType;
userinputrequest?: UserInputRequest;
}; };
type HistoryViewDataType = { type HistoryViewDataType = {
@ -586,6 +587,24 @@ type HistorySearchParams = {
filterCmds?: boolean; filterCmds?: boolean;
}; };
type UserInputRequest = {
requestid: string;
querytext: string;
responsetype: string;
};
type UserInputResponse = {
type: string;
text?: string;
confirm?: boolean;
};
type UserInputResponsePacket = {
type: string;
requestid: string;
response: UserInputResponse;
};
type RenderModeType = "normal" | "collapsed" | "expanded"; type RenderModeType = "normal" | "collapsed" | "expanded";
type WebScreen = { type WebScreen = {
@ -771,6 +790,9 @@ export type {
RenderModeType, RenderModeType,
AlertMessageType, AlertMessageType,
HistorySearchParams, HistorySearchParams,
UserInputRequest,
UserInputResponse,
UserInputResponsePacket,
ScreenLinesType, ScreenLinesType,
FocusTypeStrs, FocusTypeStrs,
HistoryTypeStrs, HistoryTypeStrs,

View File

@ -40,7 +40,7 @@ import (
"golang.org/x/mod/semver" "golang.org/x/mod/semver"
) )
const UseSshLibrary = false const UseSshLibrary = true
const RemoteTypeMShell = "mshell" const RemoteTypeMShell = "mshell"
const DefaultTerm = "xterm-256color" const DefaultTerm = "xterm-256color"

View File

@ -6,6 +6,7 @@ package remote
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"os/user" "os/user"
"strconv" "strconv"
@ -60,6 +61,14 @@ func ConnectToClient(opts *sstore.SSHOpts) (*ssh.Client, error) {
identityFile = configIdentity identityFile = configIdentity
} }
// test code
request := &sstore.UserInputRequestType{
ResponseType: "text",
QueryText: "unused",
}
response, _ := sstore.MainBus.GetUserInput(request)
log.Printf("response: %s\n", response.Text)
hostKeyCallback := ssh.InsecureIgnoreHostKey() hostKeyCallback := ssh.InsecureIgnoreHostKey()
var authMethods []ssh.AuthMethod var authMethods []ssh.AuthMethod
publicKeyAuth, err := createPublicKeyAuth(identityFile, opts.SSHPassword) publicKeyAuth, err := createPublicKeyAuth(identityFile, opts.SSHPassword)

View File

@ -20,6 +20,7 @@ const WatchScreenPacketStr = "watchscreen"
const FeInputPacketStr = "feinput" const FeInputPacketStr = "feinput"
const RemoteInputPacketStr = "remoteinput" const RemoteInputPacketStr = "remoteinput"
const CmdInputTextPacketStr = "cmdinputtext" const CmdInputTextPacketStr = "cmdinputtext"
const UserInputResponsePacketStr = "userinputresp"
type FeCommandPacketType struct { type FeCommandPacketType struct {
Type string `json:"type"` Type string `json:"type"`
@ -92,12 +93,19 @@ type CmdInputTextPacketType struct {
Text utilfn.StrWithPos `json:"text"` Text utilfn.StrWithPos `json:"text"`
} }
type UserInputResponsePacketType struct {
Type string `json:"type"`
RequestId string `json:"requestid"`
Response *sstore.UserInputResponseType `json:"response"`
}
func init() { func init() {
packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{})) packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{}))
packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{})) packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{}))
packet.RegisterPacketType(FeInputPacketStr, reflect.TypeOf(FeInputPacketType{})) packet.RegisterPacketType(FeInputPacketStr, reflect.TypeOf(FeInputPacketType{}))
packet.RegisterPacketType(RemoteInputPacketStr, reflect.TypeOf(RemoteInputPacketType{})) packet.RegisterPacketType(RemoteInputPacketStr, reflect.TypeOf(RemoteInputPacketType{}))
packet.RegisterPacketType(CmdInputTextPacketStr, reflect.TypeOf(CmdInputTextPacketType{})) packet.RegisterPacketType(CmdInputTextPacketStr, reflect.TypeOf(CmdInputTextPacketType{}))
packet.RegisterPacketType(UserInputResponsePacketStr, reflect.TypeOf(UserInputResponsePacketType{}))
} }
func (*CmdInputTextPacketType) GetType() string { func (*CmdInputTextPacketType) GetType() string {
@ -139,3 +147,7 @@ func MakeRemoteInputPacket() *RemoteInputPacketType {
func (*RemoteInputPacketType) GetType() string { func (*RemoteInputPacketType) GetType() string {
return RemoteInputPacketStr return RemoteInputPacketStr
} }
func (*UserInputResponsePacketType) GetType() string {
return UserInputResponsePacketStr
}

View File

@ -278,6 +278,15 @@ func (ws *WSState) processMessage(msgBytes []byte) error {
sstore.ScreenMemSetCmdInputText(cmdInputPk.ScreenId, cmdInputPk.Text, cmdInputPk.SeqNum) sstore.ScreenMemSetCmdInputText(cmdInputPk.ScreenId, cmdInputPk.Text, cmdInputPk.SeqNum)
return nil return nil
} }
if pk.GetType() == scpacket.UserInputResponsePacketStr {
userInputRespPk := pk.(*scpacket.UserInputResponsePacketType)
uich, ok := sstore.MainBus.UserInputCh[userInputRespPk.RequestId]
if !ok {
return fmt.Errorf("received User Input Response with invalid Id (%s): %v\n", userInputRespPk.RequestId, err)
}
uich <- userInputRespPk.Response
return nil
}
return fmt.Errorf("got ws bad message: %v", pk.GetType()) return fmt.Errorf("got ws bad message: %v", pk.GetType())
} }

View File

@ -4,10 +4,13 @@
package sstore package sstore
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"sync" "sync"
"time"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/waveshell/pkg/packet" "github.com/wavetermdev/waveterm/waveshell/pkg/packet"
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn" "github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
) )
@ -65,6 +68,7 @@ type ModelUpdate struct {
AlertMessage *AlertMessageType `json:"alertmessage,omitempty"` AlertMessage *AlertMessageType `json:"alertmessage,omitempty"`
ScreenStatusIndicator *ScreenStatusIndicatorType `json:"screenstatusindicator,omitempty"` ScreenStatusIndicator *ScreenStatusIndicatorType `json:"screenstatusindicator,omitempty"`
ScreenNumRunningCommands *ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"` ScreenNumRunningCommands *ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
UserInputRequest *UserInputRequestType `json:"userinputrequest,omitempty"`
} }
func (*ModelUpdate) UpdateType() string { func (*ModelUpdate) UpdateType() string {
@ -160,6 +164,18 @@ type HistoryInfoType struct {
Show bool `json:"show"` Show bool `json:"show"`
} }
type UserInputRequestType struct {
RequestId string `json:"requestid"`
QueryText string `json:"querytext"`
ResponseType string `json:"responsetype"`
}
type UserInputResponseType struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Confirm bool `json:"confirm,omitempty"`
}
type UpdateChannel struct { type UpdateChannel struct {
ScreenId string ScreenId string
ClientId string ClientId string
@ -176,12 +192,14 @@ func (uch UpdateChannel) Match(screenId string) bool {
type UpdateBus struct { type UpdateBus struct {
Lock *sync.Mutex Lock *sync.Mutex
Channels map[string]UpdateChannel Channels map[string]UpdateChannel
UserInputCh map[string](chan *UserInputResponseType)
} }
func MakeUpdateBus() *UpdateBus { func MakeUpdateBus() *UpdateBus {
return &UpdateBus{ return &UpdateBus{
Lock: &sync.Mutex{}, Lock: &sync.Mutex{},
Channels: make(map[string]UpdateChannel), Channels: make(map[string]UpdateChannel),
UserInputCh: make(map[string](chan *UserInputResponseType)),
} }
} }
@ -273,3 +291,46 @@ type ScreenNumRunningCommandsType struct {
ScreenId string `json:"screenid"` ScreenId string `json:"screenid"`
Num int `json:"num"` Num int `json:"num"`
} }
func (bus *UpdateBus) registerUserInputChannel() (string, chan *UserInputResponseType) {
bus.Lock.Lock()
defer bus.Lock.Unlock()
id := uuid.New().String()
uich := make(chan *UserInputResponseType)
bus.UserInputCh[id] = uich
return id, uich
}
func (bus *UpdateBus) unregisterUserInputChannel(id string) {
bus.Lock.Lock()
defer bus.Lock.Unlock()
delete(bus.UserInputCh, id)
}
func (bus *UpdateBus) GetUserInput(userInputRequest *UserInputRequestType) (*UserInputResponseType, error) {
// create context for timeout
ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancelFn()
id, uich := bus.registerUserInputChannel()
userInputRequest.RequestId = id
update := &ModelUpdate{UserInputRequest: userInputRequest}
bus.SendUpdate(update)
var response *UserInputResponseType
var err error
// prepare to receive response
select {
case resp := <-uich:
response = resp
case <-ctx.Done():
err = fmt.Errorf("timed out waiting for user input")
}
bus.unregisterUserInputChannel(id)
return response, err
}