/******************************************************************************* * 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.api.environment.server; import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.eclipse.che.api.core.BadRequestException; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.ForbiddenException; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.jsonrpc.commons.RequestTransmitter; import org.eclipse.che.api.core.model.machine.Command; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.core.util.CompositeLineConsumer; import org.eclipse.che.api.core.util.FileLineConsumer; import org.eclipse.che.api.core.util.JsonRpcEndpointIdsHolder; import org.eclipse.che.api.core.util.JsonRpcLineConsumer; import org.eclipse.che.api.core.util.LineConsumer; import org.eclipse.che.api.core.util.WebsocketLineConsumer; import org.eclipse.che.api.machine.server.exception.MachineException; import org.eclipse.che.api.machine.server.spi.Instance; import org.eclipse.che.api.machine.server.spi.InstanceProcess; import org.eclipse.che.api.machine.shared.dto.event.MachineProcessEvent; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler; import org.eclipse.che.commons.lang.concurrent.ThreadLocalPropagateContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import java.io.File; import java.io.IOException; import java.io.Reader; import java.nio.charset.Charset; import java.nio.file.Files; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static org.eclipse.che.dto.server.DtoFactory.newDto; /** * Facade for Machine process operations. * * @author gazarenkov * @author Alexander Garagatyi * @author Yevhenii Voevodin */ @Singleton public class MachineProcessManager { private static final Logger LOG = LoggerFactory.getLogger(MachineProcessManager.class); private final File machineLogsDir; private final CheEnvironmentEngine environmentEngine; private final EventService eventService; private final RequestTransmitter transmitter; private final JsonRpcEndpointIdsHolder endpointIdsHolder; @VisibleForTesting final ExecutorService executor; @Inject public MachineProcessManager(@Named("che.workspace.logs") String machineLogsDir, EventService eventService, CheEnvironmentEngine environmentEngine, RequestTransmitter transmitter, JsonRpcEndpointIdsHolder endpointIdsHolder) { this.eventService = eventService; this.machineLogsDir = new File(machineLogsDir); this.environmentEngine = environmentEngine; this.transmitter = transmitter; this.endpointIdsHolder = endpointIdsHolder; executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("MachineProcessManager-%d") .setUncaughtExceptionHandler( LoggingUncaughtExceptionHandler.getInstance()) .setDaemon(false) .build()); } /** * Execute a command in machine * * @param machineId * id of the machine where command should be executed * @param command * command that should be executed in the machine * @return {@link org.eclipse.che.api.machine.server.spi.InstanceProcess} that represents started process in machine * @throws NotFoundException * if machine with specified id not found * @throws BadRequestException * if value of required parameter is invalid * @throws MachineException * if other error occur */ public InstanceProcess exec(String workspaceId, String machineId, Command command, @Nullable String outputChannel) throws NotFoundException, MachineException, BadRequestException { requiredNotNull(machineId, "Machine ID is required"); requiredNotNull(command, "Command is required"); requiredNotNull(command.getCommandLine(), "Command line is required"); requiredNotNull(command.getName(), "Command name is required"); requiredNotNull(command.getType(), "Command type is required"); final Instance machine = environmentEngine.getMachine(workspaceId, machineId); final InstanceProcess instanceProcess = machine.createProcess(command, outputChannel); final int pid = instanceProcess.getPid(); JsonRpcLineConsumer jsonRpcLineConsumer = new JsonRpcLineConsumer(transmitter, "event:ws-agent-output:message", () -> endpointIdsHolder.getEndpointIdsByWorkspaceId(workspaceId)); LineConsumer processLogger = new CompositeLineConsumer(getProcessLogger(machineId, pid, outputChannel), jsonRpcLineConsumer); executor.execute(ThreadLocalPropagateContext.wrap(() -> { try { eventService.publish(newDto(MachineProcessEvent.class) .withEventType(MachineProcessEvent.EventType.STARTED) .withMachineId(machineId) .withProcessId(pid)); instanceProcess.start(processLogger); eventService.publish(newDto(MachineProcessEvent.class) .withEventType(MachineProcessEvent.EventType.STOPPED) .withMachineId(machineId) .withProcessId(pid)); } catch (ConflictException | MachineException error) { eventService.publish(newDto(MachineProcessEvent.class) .withEventType(MachineProcessEvent.EventType.ERROR) .withMachineId(machineId) .withProcessId(pid) .withError(error.getLocalizedMessage())); try { processLogger.writeLine(String.format("[ERROR] %s", error.getMessage())); } catch (IOException ignored) { } } finally { try { processLogger.close(); } catch (IOException ignored) { } } })); return instanceProcess; } /** * Get list of active processes from specific machine * * @param machineId * id of machine to get processes information from * @return list of {@link org.eclipse.che.api.machine.server.spi.InstanceProcess} * @throws NotFoundException * if machine with specified id not found * @throws MachineException * if other error occur */ public List<InstanceProcess> getProcesses(String workspaceId, String machineId) throws NotFoundException, MachineException { return environmentEngine.getMachine(workspaceId, machineId).getProcesses(); } /** * Stop process in machine * * @param machineId * if of the machine where process should be stopped * @param pid * id of the process that should be stopped in machine * @throws NotFoundException * if machine or process with specified id not found * @throws ForbiddenException * if process is finished already * @throws MachineException * if other error occur */ public void stopProcess(String workspaceId, String machineId, int pid) throws NotFoundException, MachineException, ForbiddenException { final InstanceProcess process = environmentEngine.getMachine(workspaceId, machineId).getProcess(pid); if (!process.isAlive()) { throw new ForbiddenException("Process finished already"); } process.kill(); eventService.publish(newDto(MachineProcessEvent.class) .withEventType(MachineProcessEvent.EventType.STOPPED) .withMachineId(machineId) .withProcessId(pid)); } /** * Gets process reader from machine by specified id. * * @param machineId * machine id whose process reader will be returned * @param pid * process id * @return reader for specified process on machine * @throws NotFoundException * if machine with specified id not found * @throws MachineException * if other error occur */ public Reader getProcessLogReader(String machineId, int pid) throws NotFoundException, MachineException { final File processLogsFile = getProcessLogsFile(machineId, pid); if (processLogsFile.isFile()) { try { return Files.newBufferedReader(processLogsFile.toPath(), Charset.defaultCharset()); } catch (IOException e) { throw new MachineException( String.format("Unable read log file for process '%s' of machine '%s'. %s", pid, machineId, e.getMessage())); } } throw new NotFoundException(String.format("Logs for process '%s' of machine '%s' are not available", pid, machineId)); } private File getProcessLogsFile(String machineId, int pid) { return new File(new File(machineLogsDir, machineId), Integer.toString(pid)); } private FileLineConsumer getProcessFileLogger(String machineId, int pid) throws MachineException { try { return new FileLineConsumer(getProcessLogsFile(machineId, pid)); } catch (IOException e) { throw new MachineException( String.format("Unable create log file for process '%s' of machine '%s'. %s", pid, machineId, e.getMessage())); } } @VisibleForTesting LineConsumer getProcessLogger(String machineId, int pid, String outputChannel) throws MachineException { return getLogger(getProcessFileLogger(machineId, pid), outputChannel); } private LineConsumer getLogger(LineConsumer fileLogger, String outputChannel) throws MachineException { if (outputChannel != null) { return new CompositeLineConsumer(fileLogger, new WebsocketLineConsumer(outputChannel)); } return fileLogger; } /** * Checks object reference is not {@code null} * * @param object * object reference to check * @param message * used as subject of exception message "{subject} required" * @throws org.eclipse.che.api.core.BadRequestException * when object reference is {@code null} */ private void requiredNotNull(Object object, String message) throws BadRequestException { if (object == null) { throw new BadRequestException(message + " required"); } } @PreDestroy private void cleanup() { boolean interrupted = false; executor.shutdown(); try { if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { executor.shutdownNow(); if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { LOG.warn("Unable terminate main pool"); } } } catch (InterruptedException e) { interrupted = true; executor.shutdownNow(); } if (interrupted) { Thread.currentThread().interrupt(); } } }