/******************************************************************************* * 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.plugin.machine.ssh.exec; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.eclipse.che.api.agent.server.terminal.WebsocketTerminalFilesPathProvider; import org.eclipse.che.api.agent.shared.model.Agent; import org.eclipse.che.api.agent.shared.model.impl.AgentImpl; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.model.machine.Command; import org.eclipse.che.api.core.util.AbstractLineConsumer; import org.eclipse.che.api.core.util.LineConsumer; import org.eclipse.che.api.core.util.ListLineConsumer; import org.eclipse.che.api.machine.server.exception.MachineException; import org.eclipse.che.api.machine.server.model.impl.CommandImpl; import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler; import org.eclipse.che.commons.lang.concurrent.ThreadLocalPropagateContext; import org.eclipse.che.plugin.machine.ssh.SshMachineInstance; import org.eclipse.che.plugin.machine.ssh.SshMachineProcess; import org.slf4j.Logger; import javax.inject.Inject; import javax.inject.Named; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.google.common.base.Strings.isNullOrEmpty; import static java.lang.String.format; import static org.slf4j.LoggerFactory.getLogger; /** * Launch exec agent in ssh machines. * * @author Alexander Garagatyi * @author Anatolii Bazko */ public class SshMachineExecAgentLauncher { private static final Logger LOG = getLogger(SshMachineExecAgentLauncher.class); // Regex to parse output of command 'uname -sm' // Consists of: // 1. named group 'os' that contains 1+ non-space characters // 2. space character // 3. named group 'architecture' that contains 1+ non-space characters private static final Pattern UNAME_OUTPUT = Pattern.compile("\\[STDOUT\\] (?<os>[\\S]+) (?<architecture>[\\S]+)"); private static final String DEFAULT_ARCHITECTURE = "linux_amd64"; private static final ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("SshAgentLauncher-%d") .setUncaughtExceptionHandler( LoggingUncaughtExceptionHandler.getInstance()) .setDaemon(true) .build()); private final WebsocketTerminalFilesPathProvider archivePathProvider; private final String terminalLocation; private final long agentMaxStartTimeMs; private final long agentPingDelayMs; private final String terminalRunCommand; @Inject public SshMachineExecAgentLauncher(@Named("che.agent.dev.max_start_time_ms") long agentMaxStartTimeMs, @Named("che.agent.dev.ping_delay_ms") long agentPingDelayMs, @Named("machine.ssh.server.terminal.location") String terminalLocation, @Named("machine.terminal_agent.run_command") String terminalRunCommand, WebsocketTerminalFilesPathProvider terminalPathProvider) { this.agentMaxStartTimeMs = agentMaxStartTimeMs; this.agentPingDelayMs = agentPingDelayMs; this.terminalRunCommand = terminalRunCommand; this.archivePathProvider = terminalPathProvider; this.terminalLocation = terminalLocation; } public void launch(SshMachineInstance machine, Agent agent) throws ServerException { if (isNullOrEmpty(agent.getScript())) { return; } try { String architecture = detectArchitecture(machine); machine.copy(archivePathProvider.getPath(architecture), terminalLocation); final AgentImpl agentCopy = new AgentImpl(agent); agentCopy.setScript(agent.getScript() + "\n" + terminalRunCommand); final SshMachineProcess process = start(machine, agentCopy); LOG.debug("Waiting for agent {} is launched. Workspace ID:{}", agentCopy.getId(), machine.getWorkspaceId()); final long pingStartTimestamp = System.currentTimeMillis(); SshProcessLaunchedChecker agentLaunchingChecker = new SshProcessLaunchedChecker("che-websocket-terminal"); while (System.currentTimeMillis() - pingStartTimestamp < agentMaxStartTimeMs) { if (agentLaunchingChecker.isLaunched(agentCopy, machine)) { return; } else { Thread.sleep(agentPingDelayMs); } } process.kill(); final String errMsg = format("Fail launching agent %s. Workspace ID:%s", agent.getName(), machine.getWorkspaceId()); LOG.error(errMsg); throw new ServerException(errMsg); } catch (MachineException e) { throw new ServerException(e.getServiceError()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ServerException(format("Launching agent %s is interrupted", agent.getName())); } catch (ConflictException e) { // should never happen throw new ServerException("Internal server error occurs on terminal launching."); } } private String detectArchitecture(SshMachineInstance machine) throws ConflictException, MachineException { // uname -sm shows OS and CPU architecture // Examples of output: // Windows 10 amd64 // MSYS_NT-6.3 x86_64 // (empty line) // Ubuntu Linux 14.04 amd64 // Linux x86_64 // OS X amd64 // Darwin x86_64 // Samsung Artik arm7 // Linux armv7l SshMachineProcess getUnameOutput = machine.createProcess(new CommandImpl("discover machine architecture", "uname -sm", null), null); ListLineConsumer lineConsumer = new ListLineConsumer(); getUnameOutput.start(lineConsumer); String unameOutput = lineConsumer.getText(); Matcher matcher = UNAME_OUTPUT.matcher(unameOutput); if (matcher.matches()) { String os = matcher.group("os").toLowerCase(); String arch = matcher.group("architecture").toLowerCase(); StringBuilder result = new StringBuilder(); if (os.contains("linux")) { result.append("linux_"); } else if (os.contains("darwin")) { result.append("darwin_"); } else if (os.contains("msys")) { result.append("windows_"); } else { LOG.error(format("Architecture discovering fails. Machine %s. uname output:%s", machine.getId(), unameOutput)); return DEFAULT_ARCHITECTURE; } if (arch.contains("x86_64")) { result.append("amd64"); } else if (arch.contains("armv7l")) { result.append("arm7"); } else if (arch.contains("armv6l")) { result.append("arm6"); } else if (arch.contains("armv5l")) { result.append("arm5"); } else { LOG.error(format("Architecture discovering fails. Machine %s. uname output:%s", machine.getId(), unameOutput)); return DEFAULT_ARCHITECTURE; } return result.toString(); } else { LOG.error(format("Architecture discovering fails. Machine %s. uname output:%s", machine.getId(), unameOutput)); return DEFAULT_ARCHITECTURE; } } protected SshMachineProcess start(SshMachineInstance machine, Agent agent) throws ServerException { Command command = new CommandImpl(agent.getId(), agent.getScript(), "agent"); SshMachineProcess process = machine.createProcess(command, null); LineConsumer lineConsumer = new AbstractLineConsumer() { @Override public void writeLine(String line) throws IOException { machine.getLogger().writeLine(line); } }; CountDownLatch countDownLatch = new CountDownLatch(1); executor.execute(ThreadLocalPropagateContext.wrap(() -> { try { countDownLatch.countDown(); process.start(lineConsumer); } catch (ConflictException | MachineException e) { try { machine.getLogger().writeLine(format("[ERROR] %s", e.getMessage())); } catch (IOException ignored) { } } finally { try { lineConsumer.close(); } catch (IOException ignored) { } } })); try { // ensure that code inside of task submitted to executor is called before end of this method countDownLatch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return process; } }