/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.terminal; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArrayInteger; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.google.gwt.user.client.ui.IsWidget; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import org.eclipse.che.api.promises.client.Operation; import org.eclipse.che.api.promises.client.OperationException; import org.eclipse.che.api.promises.client.PromiseError; import org.eclipse.che.ide.CoreLocalizationConstant; import org.eclipse.che.ide.api.action.Action; import org.eclipse.che.ide.api.machine.MachineEntity; import org.eclipse.che.ide.api.mvp.Presenter; import org.eclipse.che.ide.api.notification.NotificationManager; import org.eclipse.che.ide.collections.Jso; import org.eclipse.che.ide.websocket.WebSocket; import org.eclipse.che.ide.websocket.events.ConnectionClosedHandler; import org.eclipse.che.ide.websocket.events.ConnectionErrorHandler; import org.eclipse.che.ide.websocket.events.ConnectionOpenedHandler; import org.eclipse.che.ide.websocket.events.MessageReceivedEvent; import org.eclipse.che.ide.websocket.events.MessageReceivedHandler; import org.eclipse.che.ide.websocket.events.WebSocketClosedEvent; import org.eclipse.che.requirejs.ModuleHolder; import javax.validation.constraints.NotNull; import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.NOT_EMERGE_MODE; import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; import static org.eclipse.che.ide.websocket.events.WebSocketClosedEvent.CLOSE_NORMAL; /** * The class defines methods which contains business logic to control machine's terminal. * * @author Dmitry Shnurenko */ public class TerminalPresenter implements Presenter, TerminalView.ActionDelegate { //event which is performed when user input data into terminal private static final String DATA_EVENT_NAME = "data"; private static final int TIME_BETWEEN_CONNECTIONS = 2_000; private final TerminalView view; private final Object source; private final NotificationManager notificationManager; private final CoreLocalizationConstant locale; private final MachineEntity machine; private final TerminalInitializePromiseHolder terminalHolder; private final ModuleHolder moduleHolder; private WebSocket socket; private boolean connected; private int countRetry; private TerminalJso terminal; private TerminalStateListener terminalStateListener; private int width; private int height; @Inject public TerminalPresenter(TerminalView view, NotificationManager notificationManager, CoreLocalizationConstant locale, @Assisted MachineEntity machine, @Assisted Object source, final TerminalInitializePromiseHolder terminalHolder, final ModuleHolder moduleHolder) { this.view = view; this.source = source; view.setDelegate(this); this.notificationManager = notificationManager; this.locale = locale; this.machine = machine; connected = false; countRetry = 2; this.terminalHolder = terminalHolder; this.moduleHolder = moduleHolder; } /** * Connects to special WebSocket which allows get information from terminal on server side. The terminal is initialized only * when the method is called the first time. */ public void connect() { if (countRetry == 0) { return; } if (!connected) { terminalHolder.getInitializerPromise().then(new Operation<Void>() { @Override public void apply(Void arg) throws OperationException { connectToTerminalWebSocket(machine.getTerminalUrl()); } }).catchError(new Operation<PromiseError>() { @Override public void apply(PromiseError arg) throws OperationException { notificationManager.notify(locale.failedToConnectTheTerminal(), locale.terminalCanNotLoadScript(), FAIL, NOT_EMERGE_MODE); reconnect(); } }); } } private void reconnect() { if (countRetry <= 0) { view.showErrorMessage(locale.terminalErrorStart()); } else { view.showErrorMessage(locale.terminalTryRestarting()); new Timer() { @Override public void run() { connect(); } }.schedule(TIME_BETWEEN_CONNECTIONS); } } private void connectToTerminalWebSocket(@NotNull String wsUrl) { countRetry--; socket = WebSocket.create(wsUrl); socket.setOnMessageHandler(new MessageReceivedHandler() { @Override public void onMessageReceived(MessageReceivedEvent event) { terminal.write(event.getMessage()); } }); socket.setOnCloseHandler(new ConnectionClosedHandler() { @Override public void onClose(WebSocketClosedEvent event) { if (CLOSE_NORMAL == event.getCode()) { connected = false; terminalStateListener.onExit(); } } }); socket.setOnOpenHandler(new ConnectionOpenedHandler() { @Override public void onOpen() { JavaScriptObject terminalJso = moduleHolder.getModule("Xterm"); // if terminal was created programmatically then we don't set focus on it TerminalOptionsJso terminalOptionsJso = TerminalOptionsJso.createDefault(); if (source instanceof AddTerminalClickHandler || source instanceof Action) { terminalOptionsJso.withFocusOnOpen(true); } terminal = TerminalJso.create(terminalJso, terminalOptionsJso); connected = true; view.openTerminal(terminal); terminal.on(DATA_EVENT_NAME, new Operation<String>() { @Override public void apply(String arg) throws OperationException { Jso jso = Jso.create(); jso.addField("type", "data"); jso.addField("data", arg); socket.send(jso.serialize()); } }); } }); socket.setOnErrorHandler(new ConnectionErrorHandler() { @Override public void onError() { connected = false; if (countRetry == 0) { view.showErrorMessage(locale.terminalErrorStart()); notificationManager.notify(locale.connectionFailedWithTerminal(), locale.terminalErrorConnection(), FAIL, FLOAT_MODE); } else { reconnect(); } } }); } /** * Sends 'close' message on server side to stop terminal. */ public void stopTerminal() { if (connected) { Jso jso = Jso.create(); jso.addField("type", "close"); socket.send(jso.serialize()); } } /** {@inheritDoc} */ @Override public void go(AcceptsOneWidget container) { container.setWidget(view); } @NotNull public IsWidget getView() { return view; } public void setVisible(boolean visible) { view.setVisible(visible); } @Override public void setTerminalSize(int x, int y) { if (!connected) { return; } if (width == x && height == y) { return; } terminal.resize(x, y); width = x; height = y; Jso jso = Jso.create(); JsArrayInteger arr = Jso.createArray().cast(); arr.set(0, x); arr.set(1, y); jso.addField("type", "resize"); jso.addField("data", arr); socket.send(jso.serialize()); } /** * Sets listener that will be called when a terminal state changed */ public void setListener(TerminalStateListener listener) { this.terminalStateListener = listener; } /** Listener that will be called when a terminal state changed. */ public interface TerminalStateListener { void onExit(); } }