1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Wait for shell to bring up prompt before sending commands (#3337)

This commit is contained in:
Sebastian Malton 2021-07-20 15:25:18 -04:00 committed by GitHub
parent aeae2dcf98
commit 27f2bd7181
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 70 additions and 30 deletions

View File

@ -19,11 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { stringify } from "querystring";
import { boundMethod, base64, EventEmitter } from "../utils";
import { WebSocketApi } from "./websocket-api";
import isEqual from "lodash/isEqual";
import { isDevelopment } from "../../common/vars";
import url from "url";
import { makeObservable, observable } from "mobx";
import type { ParsedUrlQueryInput } from "querystring";
export enum TerminalChannels {
STDIN = 0,
@ -55,7 +57,9 @@ export class TerminalApi extends WebSocketApi {
protected size: { Width: number; Height: number };
public onReady = new EventEmitter<[]>();
public isReady = false;
@observable public isReady = false;
@observable public shellRunCommandsFinished = false;
public readonly url: string;
constructor(protected options: TerminalApiQuery) {
super({
@ -63,34 +67,33 @@ export class TerminalApi extends WebSocketApi {
flushOnOpen: false,
pingIntervalSeconds: 30,
});
makeObservable(this);
const { hostname, protocol, port } = location;
const query: ParsedUrlQueryInput = {
id: options.id,
};
if (options.node) {
query.node = options.node;
query.type = options.type || "node";
}
async getUrl() {
let { port } = location;
const { hostname, protocol } = location;
const { id, node } = this.options;
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
const query: TerminalApiQuery = { id };
if (port) {
port = `:${port}`;
this.url = url.format({
protocol: protocol.includes("https") ? "wss" : "ws",
hostname,
port,
pathname: "/api",
query,
slashes: true,
});
}
if (node) {
query.node = node;
query.type = "node";
}
return `${wss}${hostname}${port}/api?${stringify(query)}`;
}
async connect() {
const apiUrl = await this.getUrl();
connect() {
this.emitStatus("Connecting ...");
this.onData.addListener(this._onReady, { prepend: true });
return super.connect(apiUrl);
this.onData.addListener(this._onShellRunCommandsFinished);
super.connect(this.url);
}
destroy() {
@ -106,6 +109,24 @@ export class TerminalApi extends WebSocketApi {
this.onReady.removeAllListeners();
}
_onShellRunCommandsFinished = (data: string) => {
if (!data) {
return;
}
/**
* This is a heuistic for ditermining when a shell has finished executing
* its own rc file (or RunCommands file) such as `.bashrc` or `.zshrc`.
*
* This heuistic assumes that the prompt line of a terminal is a single line
* and ends with a whitespace character.
*/
if (data.match(/\r?\n/) === null && data.match(/\s$/)) {
this.shellRunCommandsFinished = true;
this.onData.removeListener(this._onShellRunCommandsFinished);
}
};
@boundMethod
protected _onReady(data: string) {
if (!data) return true;

View File

@ -19,12 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { autorun, observable } from "mobx";
import { autoBind, Singleton } from "../../utils";
import { autorun, observable, when } from "mobx";
import { autoBind, noop, Singleton } from "../../utils";
import { Terminal } from "./terminal";
import { TerminalApi } from "../../api/terminal-api";
import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
import { WebSocketApiState } from "../../api/websocket-api";
import { Notifications } from "../notifications";
export interface ITerminalTab extends DockTab {
node?: string; // activate node shell mode
@ -64,7 +65,7 @@ export class TerminalStore extends Singleton {
});
}
async connect(tabId: TabId) {
connect(tabId: TabId) {
if (this.isConnected(tabId)) {
return;
}
@ -104,18 +105,36 @@ export class TerminalStore extends Singleton {
return this.connections.get(tabId)?.readyState === WebSocketApiState.CLOSED;
}
sendCommand(command: string, options: { enter?: boolean; newTab?: boolean; tabId?: TabId } = {}) {
async sendCommand(command: string, options: { enter?: boolean; newTab?: boolean; tabId?: TabId } = {}) {
const { enter, newTab, tabId } = options;
const { selectTab, getTabById } = dockStore;
const tab = tabId && getTabById(tabId);
if (tab) selectTab(tabId);
if (newTab) createTerminalTab();
if (tabId) {
dockStore.selectTab(tabId);
}
if (newTab) {
const tab = createTerminalTab();
await when(() => this.connections.has(tab.id));
const rcIsFinished = when(() => this.connections.get(tab.id).shellRunCommandsFinished);
const notifyVeryLong = setTimeout(() => {
rcIsFinished.cancel();
Notifications.info("Terminal shell is taking a long time to complete startup. Please check your .rc file. Bypassing shell completion check.", {
timeout: 4_000,
});
}, 10_000);
await rcIsFinished.catch(noop);
clearTimeout(notifyVeryLong);
}
const terminalApi = this.connections.get(dockStore.selectedTabId);
if (terminalApi) {
terminalApi.sendCommand(command + (enter ? "\r" : ""));
} else {
console.warn("The selected tab is does not have a connection. Cannot send command.", { tabId: dockStore.selectedTabId, command });
}
}