/*******************************************************************************
* 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.processes;
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.Singleton;
import com.google.web.bindery.event.shared.EventBus;
import org.eclipse.che.api.machine.gwt.client.MachineServiceClient;
import org.eclipse.che.api.machine.gwt.client.events.DevMachineStateEvent;
import org.eclipse.che.api.machine.gwt.client.events.DevMachineStateHandler;
import org.eclipse.che.api.machine.shared.dto.CommandDto;
import org.eclipse.che.api.machine.shared.dto.MachineDto;
import org.eclipse.che.api.machine.shared.dto.MachineProcessDto;
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.commons.annotation.Nullable;
import org.eclipse.che.ide.api.app.AppContext;
import org.eclipse.che.ide.api.mvp.View;
import org.eclipse.che.ide.api.notification.NotificationManager;
import org.eclipse.che.ide.api.parts.HasView;
import org.eclipse.che.ide.api.parts.WorkspaceAgent;
import org.eclipse.che.ide.api.parts.base.BasePresenter;
import org.eclipse.che.ide.dto.DtoFactory;
import org.eclipse.che.ide.extension.machine.client.MachineLocalizationConstant;
import org.eclipse.che.ide.extension.machine.client.MachineResources;
import org.eclipse.che.ide.extension.machine.client.command.CommandConfiguration;
import org.eclipse.che.ide.extension.machine.client.command.CommandType;
import org.eclipse.che.ide.extension.machine.client.command.CommandTypeRegistry;
import org.eclipse.che.ide.extension.machine.client.inject.factories.EntityFactory;
import org.eclipse.che.ide.extension.machine.client.inject.factories.TerminalFactory;
import org.eclipse.che.ide.extension.machine.client.machine.Machine;
import org.eclipse.che.ide.extension.machine.client.outputspanel.console.CommandConsoleFactory;
import org.eclipse.che.ide.extension.machine.client.outputspanel.console.CommandOutputConsole;
import org.eclipse.che.ide.extension.machine.client.outputspanel.console.DefaultOutputConsole;
import org.eclipse.che.ide.extension.machine.client.perspective.terminal.TerminalPresenter;
import org.eclipse.che.ide.extension.machine.client.processes.event.ProcessFinishedEvent;
import org.eclipse.che.ide.extension.machine.client.processes.event.ProcessFinishedHandler;
import org.eclipse.che.ide.ui.dialogs.ConfirmCallback;
import org.eclipse.che.ide.ui.dialogs.DialogFactory;
import org.eclipse.che.ide.util.loging.Log;
import org.vectomatic.dom.svg.ui.SVGResource;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.che.ide.api.outputconsole.OutputConsole;
import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL;
import static org.eclipse.che.ide.extension.machine.client.perspective.terminal.TerminalPresenter.TerminalStateListener;
import static org.eclipse.che.ide.extension.machine.client.processes.ProcessTreeNode.ProcessNodeType.COMMAND_NODE;
import static org.eclipse.che.ide.extension.machine.client.processes.ProcessTreeNode.ProcessNodeType.MACHINE_NODE;
import static org.eclipse.che.ide.extension.machine.client.processes.ProcessTreeNode.ProcessNodeType.ROOT_NODE;
import static org.eclipse.che.ide.extension.machine.client.processes.ProcessTreeNode.ProcessNodeType.TERMINAL_NODE;
/**
* Presenter for managing machines process and terminals.
*
* @author Anna Shumilova
* @author Roman Nikitenko
* @author Vlad Zhukovskyi
*/
@Singleton
public class ConsolesPanelPresenter extends BasePresenter implements ConsolesPanelView.ActionDelegate, HasView, ProcessFinishedHandler {
private static final String DEFAULT_TERMINAL_NAME = "Terminal";
public static final String SSH_PORT = "22";
private final DtoFactory dtoFactory;
private final DialogFactory dialogFactory;
private final EntityFactory entityFactory;
private final TerminalFactory terminalFactory;
private final CommandConsoleFactory commandConsoleFactory;
private final NotificationManager notificationManager;
private final MachineLocalizationConstant localizationConstant;
private final ConsolesPanelView view;
private final MachineResources resources;
private final AppContext appContext;
private final MachineServiceClient machineService;
private final WorkspaceAgent workspaceAgent;
private final CommandTypeRegistry commandTypeRegistry;
ProcessTreeNode rootNode;
Map<String, TerminalPresenter> terminals;
Map<String, OutputConsole> commandConsoles;
@Inject
public ConsolesPanelPresenter(ConsolesPanelView view,
EventBus eventBus,
DtoFactory dtoFactory,
DialogFactory dialogFactory,
EntityFactory entityFactory,
TerminalFactory terminalFactory,
CommandConsoleFactory commandConsoleFactory,
CommandTypeRegistry commandTypeRegistry,
WorkspaceAgent workspaceAgent,
NotificationManager notificationManager,
MachineLocalizationConstant localizationConstant,
MachineServiceClient machineService,
MachineResources resources,
AppContext appContext) {
this.view = view;
this.terminalFactory = terminalFactory;
this.workspaceAgent = workspaceAgent;
this.commandConsoleFactory = commandConsoleFactory;
this.commandTypeRegistry = commandTypeRegistry;
this.dtoFactory = dtoFactory;
this.dialogFactory = dialogFactory;
this.notificationManager = notificationManager;
this.localizationConstant = localizationConstant;
this.resources = resources;
this.entityFactory = entityFactory;
this.appContext = appContext;
this.machineService = machineService;
this.terminals = new HashMap<>();
this.commandConsoles = new HashMap<>();
this.fetchMachines();
this.view.setDelegate(this);
this.view.setTitle(localizationConstant.viewConsolesTitle());
eventBus.addHandler(DevMachineStateEvent.TYPE, new DevMachineStateHandler() {
@Override
public void onMachineStarted(DevMachineStateEvent event) {
fetchMachines();
}
@Override
public void onMachineDestroyed(DevMachineStateEvent event) {
}
});
eventBus.addHandler(ProcessFinishedEvent.TYPE, this);
}
@Override
public void onProcessFinished() {
for (Map.Entry<String, OutputConsole> entry : commandConsoles.entrySet()) {
if (entry.getValue().isFinished()) {
view.setStopButtonVisibility(entry.getKey(), false);
}
}
}
@Override
public View getView() {
return view;
}
@NotNull
@Override
public String getTitle() {
return localizationConstant.viewConsolesTitle();
}
@Override
public void setVisible(boolean visible) {
view.setVisible(visible);
}
@Nullable
@Override
public SVGResource getTitleSVGImage() {
return resources.terminal();
}
@Override
public String getTitleToolTip() {
return localizationConstant.viewProcessesTooltip();
}
@Override
public void go(AcceptsOneWidget container) {
container.setWidget(view);
}
/** Get the list of all available machines. */
public void fetchMachines() {
String workspaceId = appContext.getWorkspaceId();
Promise<List<MachineDto>> machinesPromise = machineService.getWorkspaceMachines(workspaceId);
machinesPromise.then(new Operation<List<MachineDto>>() {
@Override
public void apply(List<MachineDto> machines) throws OperationException {
List<ProcessTreeNode> rootChildren = new ArrayList<>();
rootNode = new ProcessTreeNode(ROOT_NODE, null, null, null, rootChildren);
for (MachineDto descriptor : machines) {
if (descriptor.isDev()) {
List<ProcessTreeNode> processTreeNodes = new ArrayList<ProcessTreeNode>();
ProcessTreeNode machineNode = new ProcessTreeNode(MACHINE_NODE, rootNode, descriptor, null, processTreeNodes);
rootChildren.add(machineNode);
view.setProcessesData(rootNode);
restoreState(descriptor.getId());
}
}
}
});
}
private void restoreState(final String machineId) {
machineService.getProcesses(machineId).then(new Operation<List<MachineProcessDto>>() {
@Override
public void apply(List<MachineProcessDto> arg) throws OperationException {
for (MachineProcessDto machineProcessDto : arg) {
final CommandDto commandDto = dtoFactory.createDto(CommandDto.class)
.withName(machineProcessDto.getName())
.withCommandLine(machineProcessDto.getCommandLine())
.withType(machineProcessDto.getType());
final CommandType type = commandTypeRegistry.getCommandTypeById(commandDto.getType());
if (type != null) {
final CommandConfiguration configuration = type.getConfigurationFactory().createFromDto(commandDto);
final CommandOutputConsole console = commandConsoleFactory.create(configuration, machineId);
console.listenToOutput(machineProcessDto.getOutputChannel());
console.attachToProcess(machineProcessDto);
addCommandOutput(machineId, console);
}
}
}
}).catchError(new Operation<PromiseError>() {
@Override
public void apply(PromiseError arg) throws OperationException {
notificationManager.notify(localizationConstant.failedToGetProcesses(machineId));
}
});
}
/**
* Adds command node to process tree and displays command output
*
* @param machineId
* id of machine in which the command will be executed
* @param outputConsole
* the console for command output
*/
public void addCommandOutput(@NotNull String machineId, @NotNull OutputConsole outputConsole) {
ProcessTreeNode machineTreeNode = findProcessTreeNodeById(machineId);
if (machineTreeNode == null) {
notificationManager.notify(localizationConstant.failedToExecuteCommand(), localizationConstant.machineNotFound(machineId),
FAIL, true);
Log.error(getClass(), localizationConstant.machineNotFound(machineId));
return;
}
String commandId;
String outputConsoleTitle = outputConsole.getTitle();
ProcessTreeNode processTreeNode = getProcessTreeNodeByName(outputConsoleTitle, machineTreeNode);
if (processTreeNode != null && isCommandStopped(processTreeNode.getId())) {
commandId = processTreeNode.getId();
view.hideProcessOutput(commandId);
} else {
ProcessTreeNode commandNode = new ProcessTreeNode(COMMAND_NODE, machineTreeNode, outputConsoleTitle, outputConsole.getTitleIcon(), null);
commandId = commandNode.getId();
view.addProcessNode(commandNode);
addChildToMachineNode(commandNode, machineTreeNode);
}
updateCommandOutput(commandId, outputConsole);
resfreshStopButtonState(commandId);
workspaceAgent.setActivePart(this);
}
/**
* Adds new terminal to the processes panel
*
* @param machineId
* id of machine in which the terminal will be added
*/
@Override
public void onAddTerminal(@NotNull final String machineId) {
machineService.getMachine(machineId).then(new Operation<MachineDto>() {
@Override
public void apply(MachineDto arg) throws OperationException {
Machine machine = entityFactory.createMachine(arg);
final ProcessTreeNode machineTreeNode = findProcessTreeNodeById(machineId);
if (machineTreeNode == null) {
notificationManager.notify(localizationConstant.failedToConnectTheTerminal(),
localizationConstant.machineNotFound(machineId), FAIL, true);
Log.error(getClass(), localizationConstant.machineNotFound(machineId));
return;
}
final TerminalPresenter newTerminal = terminalFactory.create(machine);
final IsWidget terminalWidget = newTerminal.getView();
final String terminalName = getUniqueTerminalName(machineTreeNode);
final ProcessTreeNode terminalNode =
new ProcessTreeNode(TERMINAL_NODE, machineTreeNode, terminalName, resources.terminal(), null);
addChildToMachineNode(terminalNode, machineTreeNode);
final String terminalId = terminalNode.getId();
terminals.put(terminalId, newTerminal);
view.addProcessNode(terminalNode);
view.addProcessWidget(terminalId, terminalWidget);
resfreshStopButtonState(terminalId);
newTerminal.setVisible(true);
newTerminal.connect();
newTerminal.setListener(new TerminalStateListener() {
@Override
public void onExit() {
onStopProcess(terminalNode);
terminals.remove(terminalId);
}
});
}
}).catchError(new Operation<PromiseError>() {
@Override
public void apply(PromiseError arg) throws OperationException {
notificationManager.notify(localizationConstant.failedToFindMachine(machineId));
}
});
}
@Override
public void onPreviewSsh(@NotNull final String machineId) {
ProcessTreeNode machineTreeNode = findProcessTreeNodeById(machineId);
if (machineTreeNode == null) {
return;
}
MachineDto machine = (MachineDto)machineTreeNode.getData();
OutputConsole defaultConsole = commandConsoleFactory.create("SSH");
addCommandOutput(machineId, defaultConsole);
String machineName = machine.getName();
String sshServiceAddress = getSshServerAddress(machine);
String machineHost = "";
String sshPort = SSH_PORT;
if (sshServiceAddress != null) {
String[] parts = sshServiceAddress.split(":");
machineHost = parts[0];
sshPort = (parts.length == 2) ? parts[1] : sshPort;
}
if (defaultConsole instanceof DefaultOutputConsole) {
((DefaultOutputConsole)defaultConsole).printText(localizationConstant.sshConnectInfo(machineName, machineHost, sshPort));
}
}
/**
* Returns the ssh service address in format - host:port (example - localhost:32899)
*
* @param machine
* machine to retrieve address
* @return ssh service address in format host:port
*/
private String getSshServerAddress(MachineDto machine) {
if (machine.getMetadata().getServers().containsKey(SSH_PORT)) {
return machine.getMetadata().getServers().get(SSH_PORT).getAddress();
} else {
return null;
}
}
@Override
public void onCloseTerminal(@NotNull ProcessTreeNode node) {
String terminalId = node.getId();
if (terminals.containsKey(terminalId)) {
onStopProcess(node);
terminals.get(terminalId).stopTerminal();
terminals.remove(terminalId);
}
}
@Override
public void onTerminalSelected(@NotNull String terminalId) {
view.showProcessOutput(terminalId);
resfreshStopButtonState(terminalId);
}
@Override
public void onCommandSelected(@NotNull String commandId) {
view.showProcessOutput(commandId);
resfreshStopButtonState(commandId);
}
@Override
public void onStopCommandProcess(@NotNull ProcessTreeNode node) {
String commandId = node.getId();
if (commandConsoles.containsKey(commandId) && !commandConsoles.get(commandId).isFinished()) {
commandConsoles.get(commandId).onClose();
}
}
@Override
public void onCloseCommandOutputClick(@NotNull ProcessTreeNode node) {
String commandId = node.getId();
if (!commandConsoles.containsKey(commandId)) {
return;
}
OutputConsole outputConsole = commandConsoles.get(commandId);
if (outputConsole.isFinished()) {
onStopProcess(node);
commandConsoles.remove(commandId);
return;
}
dialogFactory.createConfirmDialog("", localizationConstant.outputsConsoleViewStopProcessConfirmation(outputConsole.getTitle()),
getConfirmCloseConsoleCallback(outputConsole, node), null)
.show();
}
private ConfirmCallback getConfirmCloseConsoleCallback(final OutputConsole outputConsole, final ProcessTreeNode node) {
return new ConfirmCallback() {
@Override
public void accepted() {
outputConsole.onClose();
onStopProcess(node);
commandConsoles.remove(node.getId());
}
};
}
private void onStopProcess(@NotNull ProcessTreeNode node) {
String processId = node.getId();
ProcessTreeNode parentNode = node.getParent();
int processIndex = view.getNodeIndex(processId);
if (processIndex == -1) {
return;
}
int countWidgets = terminals.size() + commandConsoles.size();
if (countWidgets == 1) {
view.hideProcessOutput(processId);
removeChildFromMachineNode(node, parentNode);
return;
}
int neighborIndex = processIndex > 0 ? processIndex - 1 : processIndex + 1;
ProcessTreeNode neighborNode = view.getNodeByIndex(neighborIndex);
String neighborNodeId = neighborNode.getId();
removeChildFromMachineNode(node, parentNode);
view.selectNode(neighborNode);
resfreshStopButtonState(neighborNodeId);
view.showProcessOutput(neighborNodeId);
view.hideProcessOutput(processId);
}
private void resfreshStopButtonState(String selectedNodeId) {
for (Map.Entry<String, OutputConsole> entry : commandConsoles.entrySet()) {
String nodeId = entry.getKey();
if (selectedNodeId.equals(nodeId) && !entry.getValue().isFinished()) {
view.setStopButtonVisibility(selectedNodeId, true);
} else {
view.setStopButtonVisibility(nodeId, false);
}
}
}
private void addChildToMachineNode(ProcessTreeNode childNode, ProcessTreeNode machineTreeNode) {
machineTreeNode.getChildren().add(childNode);
view.setProcessesData(rootNode);
view.selectNode(childNode);
}
private void removeChildFromMachineNode(ProcessTreeNode childNode, ProcessTreeNode machineTreeNode) {
view.removeProcessNode(childNode);
machineTreeNode.getChildren().remove(childNode);
view.setProcessesData(rootNode);
}
private ProcessTreeNode findProcessTreeNodeById(@NotNull String id) {
for (ProcessTreeNode processTreeNode : rootNode.getChildren()) {
if (id.equals(processTreeNode.getId())) {
return processTreeNode;
}
}
return null;
}
private String getUniqueTerminalName(ProcessTreeNode machineNode) {
String terminalName = DEFAULT_TERMINAL_NAME;
if (!isTerminalNameExist(machineNode, terminalName)) {
return DEFAULT_TERMINAL_NAME;
}
int counter = 2;
do {
terminalName = localizationConstant.viewProcessesTerminalNodeTitle(String.valueOf(counter));
counter++;
} while (isTerminalNameExist(machineNode, terminalName));
return terminalName;
}
private boolean isTerminalNameExist(ProcessTreeNode machineNode, String terminalName) {
for (ProcessTreeNode node : machineNode.getChildren()) {
if (TERMINAL_NODE == node.getType() && node.getName().equals(terminalName)) {
return true;
}
}
return false;
}
private ProcessTreeNode getProcessTreeNodeByName(String processName, ProcessTreeNode machineTreeNode) {
for (ProcessTreeNode processTreeNode : machineTreeNode.getChildren()) {
if (processTreeNode.getName().equals(processName)) {
return processTreeNode;
}
}
return null;
}
private boolean isCommandStopped(String commandId) {
return commandConsoles.containsKey(commandId) && commandConsoles.get(commandId).isFinished();
}
private void updateCommandOutput(@NotNull final String commandId, @NotNull OutputConsole outputConsole) {
commandConsoles.put(commandId, outputConsole);
outputConsole.go(new AcceptsOneWidget() {
@Override
public void setWidget(IsWidget widget) {
view.addProcessWidget(commandId, widget);
view.selectNode(view.getNodeById(commandId));
}
});
}
}