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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { stringify } from "querystring";
import { boundMethod, base64, EventEmitter } from "../utils"; import { boundMethod, base64, EventEmitter } from "../utils";
import { WebSocketApi } from "./websocket-api"; import { WebSocketApi } from "./websocket-api";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { isDevelopment } from "../../common/vars"; import { isDevelopment } from "../../common/vars";
import url from "url";
import { makeObservable, observable } from "mobx";
import type { ParsedUrlQueryInput } from "querystring";
export enum TerminalChannels { export enum TerminalChannels {
STDIN = 0, STDIN = 0,
@ -55,7 +57,9 @@ export class TerminalApi extends WebSocketApi {
protected size: { Width: number; Height: number }; protected size: { Width: number; Height: number };
public onReady = new EventEmitter<[]>(); public onReady = new EventEmitter<[]>();
public isReady = false; @observable public isReady = false;
@observable public shellRunCommandsFinished = false;
public readonly url: string;
constructor(protected options: TerminalApiQuery) { constructor(protected options: TerminalApiQuery) {
super({ super({
@ -63,34 +67,33 @@ export class TerminalApi extends WebSocketApi {
flushOnOpen: false, flushOnOpen: false,
pingIntervalSeconds: 30, pingIntervalSeconds: 30,
}); });
} makeObservable(this);
async getUrl() { const { hostname, protocol, port } = location;
let { port } = location; const query: ParsedUrlQueryInput = {
const { hostname, protocol } = location; id: options.id,
const { id, node } = this.options; };
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
const query: TerminalApiQuery = { id };
if (port) { if (options.node) {
port = `:${port}`; query.node = options.node;
query.type = options.type || "node";
} }
if (node) { this.url = url.format({
query.node = node; protocol: protocol.includes("https") ? "wss" : "ws",
query.type = "node"; hostname,
} port,
pathname: "/api",
return `${wss}${hostname}${port}/api?${stringify(query)}`; query,
slashes: true,
});
} }
async connect() { connect() {
const apiUrl = await this.getUrl();
this.emitStatus("Connecting ..."); this.emitStatus("Connecting ...");
this.onData.addListener(this._onReady, { prepend: true }); this.onData.addListener(this._onReady, { prepend: true });
this.onData.addListener(this._onShellRunCommandsFinished);
return super.connect(apiUrl); super.connect(this.url);
} }
destroy() { destroy() {
@ -106,6 +109,24 @@ export class TerminalApi extends WebSocketApi {
this.onReady.removeAllListeners(); 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 @boundMethod
protected _onReady(data: string) { protected _onReady(data: string) {
if (!data) return true; if (!data) return true;

View File

@ -19,12 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { autorun, observable } from "mobx"; import { autorun, observable, when } from "mobx";
import { autoBind, Singleton } from "../../utils"; import { autoBind, noop, Singleton } from "../../utils";
import { Terminal } from "./terminal"; import { Terminal } from "./terminal";
import { TerminalApi } from "../../api/terminal-api"; import { TerminalApi } from "../../api/terminal-api";
import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store"; import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
import { WebSocketApiState } from "../../api/websocket-api"; import { WebSocketApiState } from "../../api/websocket-api";
import { Notifications } from "../notifications";
export interface ITerminalTab extends DockTab { export interface ITerminalTab extends DockTab {
node?: string; // activate node shell mode 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)) { if (this.isConnected(tabId)) {
return; return;
} }
@ -104,18 +105,36 @@ export class TerminalStore extends Singleton {
return this.connections.get(tabId)?.readyState === WebSocketApiState.CLOSED; 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 { enter, newTab, tabId } = options;
const { selectTab, getTabById } = dockStore;
const tab = tabId && getTabById(tabId);
if (tab) selectTab(tabId); if (tabId) {
if (newTab) createTerminalTab(); 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); const terminalApi = this.connections.get(dockStore.selectedTabId);
if (terminalApi) { if (terminalApi) {
terminalApi.sendCommand(command + (enter ? "\r" : "")); terminalApi.sendCommand(command + (enter ? "\r" : ""));
} else {
console.warn("The selected tab is does not have a connection. Cannot send command.", { tabId: dockStore.selectedTabId, command });
} }
} }