/******************************************************************************* * Copyright (c) 2012-2016 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.extension.machine.client.perspective.terminal; import com.google.gwt.core.client.Callback; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JsArrayInteger; import com.google.gwt.core.client.ScriptInjector; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; 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.Promise; import org.eclipse.che.api.promises.client.PromiseError; import org.eclipse.che.api.promises.client.callback.AsyncPromiseHelper; import org.eclipse.che.ide.api.notification.NotificationManager; import org.eclipse.che.ide.collections.Jso; import org.eclipse.che.ide.extension.machine.client.MachineLocalizationConstant; import org.eclipse.che.ide.extension.machine.client.machine.Machine; import org.eclipse.che.ide.extension.machine.client.perspective.widgets.tab.content.TabPresenter; import org.eclipse.che.ide.util.loging.Log; import org.eclipse.che.ide.websocket.WebSocket; 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 javax.validation.constraints.NotNull; import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; /** * The class defines methods which contains business logic to control machine's terminal. * * @author Dmitry Shnurenko */ public class TerminalPresenter implements TabPresenter, TerminalView.ActionDelegate { //event which is performed when user input data into terminal private static final String DATA_EVENT_NAME = "data"; private static final String EXIT_COMMAND = "\nexit"; private static final int TIME_BETWEEN_CONNECTIONS = 2_000; private final TerminalView view; private final NotificationManager notificationManager; private final MachineLocalizationConstant locale; private final Machine machine; private final Timer retryConnectionTimer; private Promise<Boolean> promise; private WebSocket socket; private boolean isTerminalConnected; private int countRetry; private TerminalJso terminal; private TerminalStateListener terminalStateListener; @Inject public TerminalPresenter(TerminalView view, NotificationManager notificationManager, MachineLocalizationConstant locale, @Assisted Machine machine) { this.view = view; view.setDelegate(this); this.notificationManager = notificationManager; this.locale = locale; this.machine = machine; isTerminalConnected = false; promise = AsyncPromiseHelper.createFromAsyncRequest(new AsyncPromiseHelper.RequestCall<Boolean>() { @Override public void makeCall(final AsyncCallback<Boolean> callback) { ScriptInjector.fromUrl(GWT.getModuleBaseURL() + "term/term.js") .setWindow(ScriptInjector.TOP_WINDOW) .setCallback(new Callback<Void, Exception>() { @Override public void onFailure(Exception reason) { callback.onFailure(reason); } @Override public void onSuccess(Void result) { callback.onSuccess(true); } }).inject(); } }); countRetry = 2; retryConnectionTimer = new Timer() { @Override public void run() { connect(); countRetry--; } }; } /** * 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 (!isTerminalConnected) { promise.then(new Operation<Boolean>() { @Override public void apply(Boolean arg) throws OperationException { connectToTerminalWebSocket(machine.getTerminalUrl()); } }).catchError(new Operation<PromiseError>() { @Override public void apply(PromiseError arg) throws OperationException { isTerminalConnected = false; notificationManager.notify(locale.failedToConnectTheTerminal(), locale.terminalCanNotLoadScript(), FAIL, true); tryToReconnect(); if (arg != null) { Log.error(TerminalViewImpl.class, arg); } } }); } } private void tryToReconnect() { view.showErrorMessage(locale.terminalTryRestarting()); if (countRetry <= 0) { view.showErrorMessage(locale.terminalErrorStart()); } else { retryConnectionTimer.schedule(TIME_BETWEEN_CONNECTIONS); } } private void connectToTerminalWebSocket(@NotNull String wsUrl) { socket = WebSocket.create(wsUrl); socket.setOnOpenHandler(new ConnectionOpenedHandler() { @Override public void onOpen() { terminal = TerminalJso.create(TerminalOptionsJso.createDefault()); isTerminalConnected = 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.setOnMessageHandler(new MessageReceivedHandler() { @Override public void onMessageReceived(MessageReceivedEvent event) { String message = event.getMessage(); terminal.write(message); if (message.contains(EXIT_COMMAND) && terminalStateListener != null) { terminalStateListener.onExit(); } } }); } }); socket.setOnErrorHandler(new ConnectionErrorHandler() { @Override public void onError() { isTerminalConnected = false; notificationManager.notify(locale.connectionFailedWithTerminal(), locale.terminalErrorConnection(), FAIL, true); tryToReconnect(); } }); } /** * Sends 'exit' command on server side to stop terminal. */ public void stopTerminal() { if (isTerminalConnected) { Jso jso = Jso.create(); jso.addField("type", "data"); jso.addField("data", "exit\n"); socket.send(jso.serialize()); } } /** {@inheritDoc} */ @Override public void go(AcceptsOneWidget container) { container.setWidget(view); } /** {@inheritDoc} */ @Override @NotNull public IsWidget getView() { return view; } /** {@inheritDoc} */ @Override public void setVisible(boolean visible) { view.setVisible(visible); } @Override public void setTerminalSize(int x, int y) { if (!isTerminalConnected) { return; } terminal.resize(x, y); terminal.focus(); 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()); } /** Set focus on terminal */ public void setFocus() { if (!isTerminalConnected) { return; } terminal.focus(); } /** * 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(); } }