User Input (#119)

Port the User Input feature from the previous version of the app. This
is currently being used to verify a few different prompts for ssh.
This commit is contained in:
Sylvie Crowe 2024-07-18 15:21:33 -07:00 committed by GitHub
parent 01d61dabec
commit f3743f90ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 438 additions and 126 deletions

View File

@ -6,7 +6,7 @@
top: 0;
left: 0;
width: 100vw;
height: 100vh;
height: 100%;
z-index: 100;
background-color: rgba(21, 23, 21, 0.7);
@ -15,8 +15,7 @@
flex-direction: column;
border-radius: 10px;
padding: 0;
width: 80vw;
height: 80vh;
width: 80%;
margin-top: 25vh;
margin-left: auto;
margin-right: auto;

View File

@ -70,7 +70,7 @@ interface WaveModalProps {
function WaveModal({ title, description, onSubmit, onCancel, buttonLabel = "Ok", children }: WaveModalProps) {
return (
<Modal id="text-box-modal" onClickOut={onCancel}>
<Modal onClickOut={onCancel}>
<ModalHeader title={title} description={description} />
<ModalContent>{children}</ModalContent>
<ModalFooter>

View File

@ -0,0 +1,37 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.userinput-body {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1rem;
font: var(--fixed-font);
color: var(--main-text-color);
.userinput-markdown {
color: var(--main-text-color);
}
.userinput-text {
}
.userinput-inputbox {
resize: none;
background-color: var(--panel-bg-color);
border-radius: 6px;
margin: 0;
border: var(--border-color);
padding: 5px 0 5px 16px;
min-height: 30px;
&:hover {
cursor: text;
}
&:focus {
outline-color: var(--accent-color);
}
}
}

View File

@ -0,0 +1,133 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Markdown } from "@/element/markdown";
import { WaveModal } from "@/element/modal";
import { atoms } from "@/store/global";
import * as keyutil from "@/util/keyutil";
import * as jotai from "jotai";
import * as React from "react";
import { UserInputService } from "../store/services";
import "./userinputmodal.less";
export const UserInputModal = (userInputRequest: UserInputRequest) => {
const setModals = jotai.useSetAtom(atoms.userInput);
const [responseText, setResponseText] = React.useState("");
const [countdown, setCountdown] = React.useState(Math.floor(userInputRequest.timeoutms / 1000));
const checkboxStatus = React.useRef(false);
const handleSendCancel = React.useCallback(() => {
UserInputService.SendUserInputResponse({
type: "userinputresp",
requestid: userInputRequest.requestid,
errormsg: "Canceled by the user",
});
setModals((prev) => {
prev.pop();
return [...prev];
});
}, [responseText, userInputRequest]);
const handleSendText = React.useCallback(() => {
UserInputService.SendUserInputResponse({
type: "userinputresp",
requestid: userInputRequest.requestid,
text: responseText,
checkboxstat: checkboxStatus.current,
});
setModals((prev) => {
prev.pop();
return [...prev];
});
}, [responseText, userInputRequest]);
const handleSendConfirm = React.useCallback(() => {
UserInputService.SendUserInputResponse({
type: "userinputresp",
requestid: userInputRequest.requestid,
confirm: true,
checkboxstat: checkboxStatus.current,
});
setModals((prev) => {
prev.pop();
return [...prev];
});
}, [userInputRequest]);
const handleSubmit = React.useCallback(() => {
switch (userInputRequest.responsetype) {
case "text":
handleSendText();
break;
case "confirm":
handleSendConfirm();
break;
}
}, [handleSendConfirm, handleSendText, userInputRequest.responsetype]);
const handleKeyDown = React.useCallback(
(waveEvent: WaveKeyboardEvent): boolean => {
if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
handleSendCancel();
return;
}
if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
handleSubmit();
return true;
}
},
[handleSendCancel, handleSubmit]
);
const queryText = React.useMemo(() => {
if (userInputRequest.markdown) {
return <Markdown text={userInputRequest.querytext} className="userinput-markdown" />;
}
return <span className="userinput-text">{userInputRequest.querytext}</span>;
}, [userInputRequest.markdown, userInputRequest.querytext]);
const inputBox = React.useMemo(() => {
if (userInputRequest.responsetype === "confirm") {
return <></>;
}
return (
<input
type={userInputRequest.publictext ? "text" : "password"}
onChange={(e) => setResponseText(e.target.value)}
value={responseText}
maxLength={400}
className="userinput-inputbox"
autoFocus={true}
onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)}
/>
);
}, [userInputRequest.responsetype, userInputRequest.publictext, responseText, handleKeyDown, setResponseText]);
React.useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
if (countdown == 0) {
timeout = setTimeout(() => {
handleSendCancel();
}, 300);
} else {
timeout = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
}
return () => clearTimeout(timeout);
}, [countdown]);
return (
<WaveModal
title={userInputRequest.title + ` (${countdown}s)`}
onSubmit={() => handleSubmit()}
onCancel={() => handleSendCancel()}
>
<div className="userinput-body">
{queryText}
{inputBox}
</div>
</WaveModal>
);
};

View File

@ -81,6 +81,7 @@ const activeTabIdAtom: jotai.Atom<string> = jotai.atom((get) => {
}
return windowData.activetabid;
});
const userInputAtom = jotai.atom([]) as jotai.PrimitiveAtom<Array<UserInputRequest>>;
const atoms = {
// initialized in wave.ts (will not be null inside of application)
@ -93,6 +94,7 @@ const atoms = {
settingsConfigAtom: settingsConfigAtom,
tabAtom: tabAtom,
activeTabId: activeTabIdAtom,
userInput: userInputAtom,
};
// key is "eventType" or "eventType|oref"
@ -208,6 +210,13 @@ function handleWSEventMessage(msg: WSEventType) {
console.log("config", data);
return;
}
if (msg.eventtype == "userinput") {
// handle user input
const data: UserInputRequest = msg.data;
console.log(data);
globalStore.set(userInputAtom, (prev) => [...prev, data]);
return;
}
if (msg.eventtype == "blockfile") {
const fileData: WSFileEventData = msg.data;
const fileSubject = getFileSubject(fileData.zoneid, fileData.filename);

View File

@ -126,6 +126,15 @@ class ObjectServiceType {
export const ObjectService = new ObjectServiceType();
// userinputservice.UserInputService (userinput)
class UserInputServiceType {
SendUserInputResponse(arg1: UserInputResponse): Promise<void> {
return WOS.callBackendService("userinput", "SendUserInputResponse", Array.from(arguments))
}
}
export const UserInputService = new UserInputServiceType();
// windowservice.WindowService (window)
class WindowServiceType {
// @returns object updates

View File

@ -3,6 +3,7 @@
import { TabBar } from "@/app/tab/tabbar";
import { TabContent } from "@/app/tab/tabcontent";
import { UserInputModal } from "@/element/userinputmodal";
import { atoms, createBlock } from "@/store/global";
import * as services from "@/store/services";
import * as util from "@/util/util";
@ -106,9 +107,11 @@ const Widgets = React.memo(() => {
const WorkspaceElem = React.memo(() => {
const windowData = jotai.useAtomValue(atoms.waveWindow);
const activeTabId = windowData?.activetabid;
const modals = jotai.useAtomValue(atoms.userInput);
const ws = jotai.useAtomValue(atoms.workspace);
return (
<div className="workspace">
{modals.length > 0 && <UserInputModal {...modals[modals.length - 1]} />}
<TabBar key={ws.oid} workspace={ws} />
<div className="workspace-tabcontent">
{activeTabId == "" ? (

View File

@ -262,6 +262,28 @@ declare global {
activetabid: string;
};
// userinput.UserInputRequest
type UserInputRequest = {
requestid: string;
querytext: string;
responsetype: string;
title: string;
markdown: boolean;
timeoutms: number;
checkboxmsg: string;
publictext: boolean;
};
// userinput.UserInputResponse
type UserInputResponse = {
type: string;
requestid: string;
text?: string;
confirm?: boolean;
errormsg?: string;
checkboxstat?: boolean;
};
type WSCommandType = {
wscommand: string;
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );

View File

@ -18,6 +18,7 @@ const (
WSEvent_WaveObjUpdate = "waveobj:update"
WSEvent_BlockFile = "blockfile"
WSEvent_Config = "config"
WSEvent_UserInput = "userinput"
WSEvent_BlockControllerStatus = "blockcontroller:status"
WSEvent_LayoutAction = "layoutaction"
WSEvent_ElectronNewWindow = "electron:newwindow"

View File

@ -1,4 +1,4 @@
// Copyright 2023-2024, Command Line Inc.
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package remote
@ -19,8 +19,10 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/kevinburke/ssh_config"
"github.com/wavetermdev/thenextwave/pkg/userinput"
"github.com/wavetermdev/thenextwave/pkg/wavebase"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
@ -115,28 +117,25 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
return createDummySigner()
}
return nil, fmt.Errorf("unimplemented: userinput createPublicKeyCallback") //todo
/*
request := &userinput.UserInputRequestType{
ResponseType: "text",
QueryText: fmt.Sprintf("Enter passphrase for the SSH key: %s", identityFile),
Title: "Publickey Auth + Passphrase",
}
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
defer cancelFn()
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
if err != nil {
// this is an error where we actually do want to stop
// trying keys
return nil, UserInputCancelError{Err: err}
}
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(response.Text))
if err != nil {
// skip this key and try with the next
return createDummySigner()
}
return []ssh.Signer{signer}, err
*/
request := &userinput.UserInputRequest{
ResponseType: "text",
QueryText: fmt.Sprintf("Enter passphrase for the SSH key: %s", identityFile),
Title: "Publickey Auth + Passphrase",
}
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
defer cancelFn()
response, err := userinput.GetUserInput(ctx, request)
if err != nil {
// this is an error where we actually do want to stop
// trying keys
return nil, UserInputCancelError{Err: err}
}
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(response.Text))
if err != nil {
// skip this key and try with the next
return createDummySigner()
}
return []ssh.Signer{signer}, err
}
}
@ -151,26 +150,23 @@ func createDefaultPasswordCallbackPrompt(password string) func() (secret string,
func createInteractivePasswordCallbackPrompt(connCtx context.Context, remoteDisplayName string) func() (secret string, err error) {
return func() (secret string, err error) {
return "", fmt.Errorf("unimplemented: userinput createInteractivePasswordCallbackPrompt") //todo
/*
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
defer cancelFn()
queryText := fmt.Sprintf(
"Password Authentication requested from connection \n"+
"%s\n\n"+
"Password:", remoteDisplayName)
request := &userinput.UserInputRequestType{
ResponseType: "text",
QueryText: queryText,
Markdown: true,
Title: "Password Authentication",
}
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
if err != nil {
return "", err
}
return response.Text, nil
*/
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
defer cancelFn()
queryText := fmt.Sprintf(
"Password Authentication requested from connection \n"+
"%s\n\n"+
"Password:", remoteDisplayName)
request := &userinput.UserInputRequest{
ResponseType: "text",
QueryText: queryText,
Markdown: true,
Title: "Password Authentication",
}
response, err := userinput.GetUserInput(ctx, request)
if err != nil {
return "", err
}
return response.Text, nil
}
}
@ -219,27 +215,24 @@ func createInteractiveKbdInteractiveChallenge(connCtx context.Context, remoteNam
func promptChallengeQuestion(connCtx context.Context, question string, echo bool, remoteName string) (answer string, err error) {
// limited to 15 seconds for some reason. this should be investigated more
// in the future
return "", fmt.Errorf("unimplemented: userinput promptChallengeQuestion") //todo
/*
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
defer cancelFn()
queryText := fmt.Sprintf(
"Keyboard Interactive Authentication requested from connection \n"+
"%s\n\n"+
"%s", remoteName, question)
request := &userinput.UserInputRequestType{
ResponseType: "text",
QueryText: queryText,
Markdown: true,
Title: "Keyboard Interactive Authentication",
PublicText: echo,
}
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
if err != nil {
return "", err
}
return response.Text, nil
*/
ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second)
defer cancelFn()
queryText := fmt.Sprintf(
"Keyboard Interactive Authentication requested from connection \n"+
"%s\n\n"+
"%s", remoteName, question)
request := &userinput.UserInputRequest{
ResponseType: "text",
QueryText: queryText,
Markdown: true,
Title: "Keyboard Interactive Authentication",
PublicText: echo,
}
response, err := userinput.GetUserInput(ctx, request)
if err != nil {
return "", err
}
return response.Text, nil
}
func createCombinedKbdInteractiveChallenge(connCtx context.Context, password string, remoteName string) ssh.KeyboardInteractiveChallenge {
@ -263,11 +256,10 @@ func openKnownHostsForEdit(knownHostsFilename string) (*os.File, error) {
return os.OpenFile(knownHostsFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
}
/*
func writeToKnownHosts(knownHostsFile string, newLine string, getUserVerification func() (*userinput.UserInputResponsePacketType, error)) error {
func writeToKnownHosts(knownHostsFile string, newLine string, getUserVerification func() (*userinput.UserInputResponse, error)) error {
if getUserVerification == nil {
getUserVerification = func() (*userinput.UserInputResponsePacketType, error) {
return &userinput.UserInputResponsePacketType{
getUserVerification = func() (*userinput.UserInputResponse, error) {
return &userinput.UserInputResponse{
Type: "confirm",
Confirm: true,
}, nil
@ -303,10 +295,8 @@ func writeToKnownHosts(knownHostsFile string, newLine string, getUserVerificatio
}
return f.Close()
}
*/
/* todo
func createUnknownKeyVerifier(knownHostsFile string, hostname string, remote string, key ssh.PublicKey) func() (*userinput.UserInputResponsePacketType, error) {
func createUnknownKeyVerifier(knownHostsFile string, hostname string, remote string, key ssh.PublicKey) func() (*userinput.UserInputResponse, error) {
base64Key := base64.StdEncoding.EncodeToString(key.Marshal())
queryText := fmt.Sprintf(
"The authenticity of host '%s (%s)' can't be established "+
@ -316,22 +306,20 @@ func createUnknownKeyVerifier(knownHostsFile string, hostname string, remote str
"**Would you like to continue connecting?** If so, the key will be permanently "+
"added to the file %s "+
"to protect from future man-in-the-middle attacks.", hostname, remote, key.Type(), base64Key, knownHostsFile)
request := &userinput.UserInputRequestType{
request := &userinput.UserInputRequest{
ResponseType: "confirm",
QueryText: queryText,
Markdown: true,
Title: "Known Hosts Key Missing",
}
return func() (*userinput.UserInputResponsePacketType, error) {
return func() (*userinput.UserInputResponse, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFn()
return userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
return userinput.GetUserInput(ctx, request)
}
}
*/
/*
func createMissingKnownHostsVerifier(knownHostsFile string, hostname string, remote string, key ssh.PublicKey) func() (*userinput.UserInputResponsePacketType, error) {
func createMissingKnownHostsVerifier(knownHostsFile string, hostname string, remote string, key ssh.PublicKey) func() (*userinput.UserInputResponse, error) {
base64Key := base64.StdEncoding.EncodeToString(key.Marshal())
queryText := fmt.Sprintf(
"The authenticity of host '%s (%s)' can't be established "+
@ -342,19 +330,18 @@ func createMissingKnownHostsVerifier(knownHostsFile string, hostname string, rem
"- %s will be created \n"+
"- the key will be added to %s\n\n"+
"This will protect from future man-in-the-middle attacks.", hostname, remote, key.Type(), base64Key, knownHostsFile, knownHostsFile)
request := &userinput.UserInputRequestType{
request := &userinput.UserInputRequest{
ResponseType: "confirm",
QueryText: queryText,
Markdown: true,
Title: "Known Hosts File Missing",
}
return func() (*userinput.UserInputResponsePacketType, error) {
return func() (*userinput.UserInputResponse, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFn()
return userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
return userinput.GetUserInput(ctx, request)
}
}
*/
func lineContainsMatch(line []byte, matches [][]byte) bool {
for _, match := range matches {
@ -444,41 +431,38 @@ func createHostKeyCallback(opts *SSHOpts) (ssh.HostKeyCallback, error) {
// the key was not found
// try to write to a file that could be read
//err := fmt.Errorf("placeholder, should not be returned") // a null value here can cause problems with empty slice
return fmt.Errorf("unimplemented: waveHostKeyCallback key not found") //todo
/*
for _, filename := range knownHostsFiles {
newLine := knownhosts.Line([]string{knownhosts.Normalize(hostname)}, key)
getUserVerification := createUnknownKeyVerifier(filename, hostname, remote.String(), key)
err = writeToKnownHosts(filename, newLine, getUserVerification)
if err == nil {
break
}
if serr, ok := err.(UserInputCancelError); ok {
return serr
}
}
err := fmt.Errorf("placeholder, should not be returned") // a null value here can cause problems with empty slice
for _, filename := range knownHostsFiles {
newLine := knownhosts.Line([]string{knownhosts.Normalize(hostname)}, key)
getUserVerification := createUnknownKeyVerifier(filename, hostname, remote.String(), key)
err = writeToKnownHosts(filename, newLine, getUserVerification)
if err == nil {
break
}
if serr, ok := err.(UserInputCancelError); ok {
return serr
}
}
// try to write to a file that could not be read (file likely doesn't exist)
// should catch cases where there is no known_hosts file
if err != nil {
for _, filename := range unreadableFiles {
newLine := knownhosts.Line([]string{knownhosts.Normalize(hostname)}, key)
getUserVerification := createMissingKnownHostsVerifier(filename, hostname, remote.String(), key)
err = writeToKnownHosts(filename, newLine, getUserVerification)
if err == nil {
knownHostsFiles = []string{filename}
break
}
if serr, ok := err.(UserInputCancelError); ok {
return serr
}
}
// try to write to a file that could not be read (file likely doesn't exist)
// should catch cases where there is no known_hosts file
if err != nil {
for _, filename := range unreadableFiles {
newLine := knownhosts.Line([]string{knownhosts.Normalize(hostname)}, key)
getUserVerification := createMissingKnownHostsVerifier(filename, hostname, remote.String(), key)
err = writeToKnownHosts(filename, newLine, getUserVerification)
if err == nil {
knownHostsFiles = []string{filename}
break
}
if serr, ok := err.(UserInputCancelError); ok {
return serr
}
}
if err != nil {
return fmt.Errorf("unable to create new knownhost key: %e", err)
}
*/
}
if err != nil {
return fmt.Errorf("unable to create new knownhost key: %e", err)
}
} else {
// the key changed
correctKeyFingerprint := base64.StdEncoding.EncodeToString(key.Marshal())

View File

@ -13,6 +13,7 @@ import (
"github.com/wavetermdev/thenextwave/pkg/service/clientservice"
"github.com/wavetermdev/thenextwave/pkg/service/fileservice"
"github.com/wavetermdev/thenextwave/pkg/service/objectservice"
"github.com/wavetermdev/thenextwave/pkg/service/userinputservice"
"github.com/wavetermdev/thenextwave/pkg/service/windowservice"
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
@ -22,11 +23,12 @@ import (
)
var ServiceMap = map[string]any{
"block": blockservice.BlockServiceInstance,
"object": &objectservice.ObjectService{},
"file": &fileservice.FileService{},
"client": &clientservice.ClientService{},
"window": &windowservice.WindowService{},
"block": blockservice.BlockServiceInstance,
"object": &objectservice.ObjectService{},
"file": &fileservice.FileService{},
"client": &clientservice.ClientService{},
"window": &windowservice.WindowService{},
"userinput": &userinputservice.UserInputService{},
}
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()

View File

@ -0,0 +1,18 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package userinputservice
import (
"github.com/wavetermdev/thenextwave/pkg/userinput"
)
type UserInputService struct {
}
func (uis *UserInputService) SendUserInputResponse(response *userinput.UserInputResponse) {
select {
case userinput.MainUserInputHandler.Channels[response.RequestId] <- response:
default:
}
}

View File

@ -17,6 +17,7 @@ import (
"strings"
"sync"
"syscall"
"time"
"github.com/creack/pty"
"github.com/wavetermdev/thenextwave/pkg/remote"
@ -114,7 +115,7 @@ func checkCwd(cwd string) error {
var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
func StartRemoteShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsType, remoteName string) (*ShellProc, error) {
ctx, cancelFunc := context.WithCancel(context.Background())
ctx, cancelFunc := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFunc()
var shellPath string

View File

@ -14,6 +14,7 @@ import (
"github.com/wavetermdev/thenextwave/pkg/filestore"
"github.com/wavetermdev/thenextwave/pkg/service"
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
"github.com/wavetermdev/thenextwave/pkg/userinput"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wconfig"
"github.com/wavetermdev/thenextwave/pkg/web/webcmd"
@ -39,6 +40,7 @@ var ExtraTypes = []any{
wconfig.WatcherUpdate{},
wshutil.RpcMessage{},
wshrpc.WshServerCommandMeta{},
userinput.UserInputRequest{},
}
// add extra type unions to generate here

View File

@ -0,0 +1,92 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package userinput
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/google/uuid"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
)
var MainUserInputHandler = UserInputHandler{Channels: make(map[string](chan *UserInputResponse), 1)}
type UserInputRequest struct {
RequestId string `json:"requestid"`
QueryText string `json:"querytext"`
ResponseType string `json:"responsetype"`
Title string `json:"title"`
Markdown bool `json:"markdown"`
TimeoutMs int `json:"timeoutms"`
CheckBoxMsg string `json:"checkboxmsg"`
PublicText bool `json:"publictext"`
}
type UserInputResponse struct {
Type string `json:"type"`
RequestId string `json:"requestid"`
Text string `json:"text,omitempty"`
Confirm bool `json:"confirm,omitempty"`
ErrorMsg string `json:"errormsg,omitempty"`
CheckboxStat bool `json:"checkboxstat,omitempty"`
}
type UserInputHandler struct {
Lock sync.Mutex
Channels map[string](chan *UserInputResponse)
}
func (ui *UserInputHandler) registerChannel() (string, chan *UserInputResponse) {
ui.Lock.Lock()
defer ui.Lock.Unlock()
id := uuid.New().String()
uich := make(chan *UserInputResponse, 1)
ui.Channels[id] = uich
return id, uich
}
func (ui *UserInputHandler) unregisterChannel(id string) {
ui.Lock.Lock()
defer ui.Lock.Unlock()
delete(ui.Channels, id)
}
func (ui *UserInputHandler) sendRequestToFrontend(request *UserInputRequest) {
eventbus.SendEvent(eventbus.WSEventType{
EventType: eventbus.WSEvent_UserInput,
Data: request,
})
}
func GetUserInput(ctx context.Context, request *UserInputRequest) (*UserInputResponse, error) {
id, uiCh := MainUserInputHandler.registerChannel()
defer MainUserInputHandler.unregisterChannel(id)
request.RequestId = id
deadline, _ := ctx.Deadline()
request.TimeoutMs = int(time.Until(deadline).Milliseconds()) - 500
MainUserInputHandler.sendRequestToFrontend(request)
var response *UserInputResponse
var err error
select {
case resp := <-uiCh:
log.Printf("checking received: %v", resp.RequestId)
response = resp
case <-ctx.Done():
return nil, fmt.Errorf("timed out waiting for user input")
}
if response.ErrorMsg != "" {
err = fmt.Errorf(response.ErrorMsg)
}
return response, err
}

View File

@ -40,13 +40,13 @@ type WatcherUpdate struct {
func readFileContents(filePath string, getDefaults func() SettingsConfigType) (*SettingsConfigType, error) {
if getDefaults == nil {
log.Printf("oopsie")
log.Printf("should not happen")
return nil, fmt.Errorf("watcher started without defaults")
}
content := getDefaults()
data, err := os.ReadFile(filePath)
if err != nil {
log.Printf("doopsie: %v", err)
log.Printf("could not read settings file: %v", err)
return nil, err
}
if err := json.Unmarshal(data, &content); err != nil {