mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-21 21:32:13 +01:00
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:
parent
01d61dabec
commit
f3743f90ec
@ -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;
|
||||
|
@ -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>
|
||||
|
37
frontend/app/element/userinputmodal.less
Normal file
37
frontend/app/element/userinputmodal.less
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
133
frontend/app/element/userinputmodal.tsx
Normal file
133
frontend/app/element/userinputmodal.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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 == "" ? (
|
||||
|
22
frontend/types/gotypes.d.ts
vendored
22
frontend/types/gotypes.d.ts
vendored
@ -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 );
|
||||
|
@ -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"
|
||||
|
@ -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,16 +117,14 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
return createDummySigner()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unimplemented: userinput createPublicKeyCallback") //todo
|
||||
/*
|
||||
request := &userinput.UserInputRequestType{
|
||||
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, scbus.MainRpcBus, request)
|
||||
response, err := userinput.GetUserInput(ctx, request)
|
||||
if err != nil {
|
||||
// this is an error where we actually do want to stop
|
||||
// trying keys
|
||||
@ -136,7 +136,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
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{
|
||||
request := &userinput.UserInputRequest{
|
||||
ResponseType: "text",
|
||||
QueryText: queryText,
|
||||
Markdown: true,
|
||||
Title: "Password Authentication",
|
||||
}
|
||||
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
|
||||
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{
|
||||
request := &userinput.UserInputRequest{
|
||||
ResponseType: "text",
|
||||
QueryText: queryText,
|
||||
Markdown: true,
|
||||
Title: "Keyboard Interactive Authentication",
|
||||
PublicText: echo,
|
||||
}
|
||||
response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request)
|
||||
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,9 +431,7 @@ 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
|
||||
/*
|
||||
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)
|
||||
@ -478,7 +463,6 @@ func createHostKeyCallback(opts *SSHOpts) (ssh.HostKeyCallback, error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create new knownhost key: %e", err)
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
// the key changed
|
||||
correctKeyFingerprint := base64.StdEncoding.EncodeToString(key.Marshal())
|
||||
|
@ -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"
|
||||
@ -27,6 +28,7 @@ var ServiceMap = map[string]any{
|
||||
"file": &fileservice.FileService{},
|
||||
"client": &clientservice.ClientService{},
|
||||
"window": &windowservice.WindowService{},
|
||||
"userinput": &userinputservice.UserInputService{},
|
||||
}
|
||||
|
||||
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
|
18
pkg/service/userinputservice/userinputservice.go
Normal file
18
pkg/service/userinputservice/userinputservice.go
Normal 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:
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
92
pkg/userinput/userinput.go
Normal file
92
pkg/userinput/userinput.go
Normal 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
|
||||
}
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user