CHE-3621 Move SSH machine implementation to separate plugin (#3946)

6.19.x
Max Shaposhnik 2017-02-02 17:25:26 +02:00 committed by GitHub
parent 24b11c3726
commit a346a5f8b7
13 changed files with 445 additions and 299 deletions

View File

@ -48,14 +48,6 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-machine</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

View File

@ -1,134 +0,0 @@
/*******************************************************************************
* 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.agent;
import org.eclipse.che.api.agent.server.terminal.WebsocketTerminalFilesPathProvider;
import org.eclipse.che.api.agent.shared.model.Agent;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ServerException;
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.api.machine.server.spi.Instance;
import org.eclipse.che.api.machine.server.spi.InstanceProcess;
import org.slf4j.Logger;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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 extends ExecAgentLauncher {
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 final WebsocketTerminalFilesPathProvider archivePathProvider;
private final String terminalLocation;
@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) {
super(agentMaxStartTimeMs, agentPingDelayMs, terminalRunCommand);
this.archivePathProvider = terminalPathProvider;
this.terminalLocation = terminalLocation;
}
@Override
public String getMachineType() {
return "ssh";
}
@Override
public void launch(Instance machine, Agent agent) throws ServerException {
try {
String architecture = detectArchitecture(machine);
machine.copy(archivePathProvider.getPath(architecture), terminalLocation);
super.launch(machine, agent);
} catch (ConflictException e) {
// should never happen
throw new ServerException("Internal server error occurs on terminal launching.");
}
}
private String detectArchitecture(Instance 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
InstanceProcess 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;
}
}
}

View File

@ -23,7 +23,6 @@ import org.eclipse.che.api.agent.LSPythonAgent;
import org.eclipse.che.api.agent.LSTypeScriptAgent;
import org.eclipse.che.api.agent.SshAgent;
import org.eclipse.che.api.agent.SshAgentLauncher;
import org.eclipse.che.api.agent.SshMachineExecAgentLauncher;
import org.eclipse.che.api.agent.UnisonAgent;
import org.eclipse.che.api.agent.WsAgent;
import org.eclipse.che.api.agent.WsAgentLauncher;
@ -136,7 +135,6 @@ public class WsMasterModule extends AbstractModule {
Multibinder<AgentLauncher> launchers = Multibinder.newSetBinder(binder(), AgentLauncher.class);
launchers.addBinding().to(WsAgentLauncher.class);
launchers.addBinding().to(ExecAgentLauncher.class);
launchers.addBinding().to(SshMachineExecAgentLauncher.class);
launchers.addBinding().to(SshAgentLauncher.class);
bindConstant().annotatedWith(Names.named("machine.ws_agent.run_command"))

View File

@ -30,6 +30,10 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
@ -54,6 +58,10 @@
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-agent-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>
@ -66,6 +74,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-model</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-workspace</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-annotations</artifactId>
@ -74,6 +86,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-lang</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>

View File

@ -10,22 +10,38 @@
*******************************************************************************/
package org.eclipse.che.plugin.machine.ssh;
import com.google.inject.assistedinject.Assisted;
import com.jcraft.jsch.JSch;
import org.eclipse.che.api.core.model.machine.Command;
import org.eclipse.che.api.core.model.machine.Machine;
import org.eclipse.che.api.core.model.machine.ServerConf;
import org.eclipse.che.api.core.util.LineConsumer;
import org.eclipse.che.api.machine.server.exception.MachineException;
import org.eclipse.che.api.machine.server.spi.Instance;
import org.eclipse.che.plugin.machine.ssh.jsch.JschSshClient;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Map;
import java.util.Set;
/**
* Provides ssh machine implementation instances.
*
* @author Alexander Garagatyi
* @author Max Shaposhnik
*/
public interface SshMachineFactory {
public class SshMachineFactory {
private final int connectionTimeoutMs;
private final Set<ServerConf> machinesServers;
@Inject
public SshMachineFactory(@Named("che.workspace.ssh_connection_timeout_ms") int connectionTimeoutMs,
@Named("machine.ssh.machine_servers") Set<ServerConf> machinesServers) {
this.connectionTimeoutMs = connectionTimeoutMs;
this.machinesServers = machinesServers;
}
/**
* Creates {@link SshClient} to communicate with machine over SSH protocol.
@ -35,31 +51,39 @@ public interface SshMachineFactory {
* @param envVars
* environment variables that should be injected into machine
*/
SshClient createSshClient(@Assisted SshMachineRecipe sshMachineRecipe,
@Assisted Map<String, String> envVars);
public SshClient createSshClient(SshMachineRecipe sshMachineRecipe, Map<String, String> envVars) {
return new JschSshClient(sshMachineRecipe, envVars, new JSch(), connectionTimeoutMs);
}
/**
* Creates ssh machine implementation of {@link Instance}.
* Creates ssh machine implementation instance.
*
* @param machine description of machine
* @param sshClient ssh client of machine
* @param outputConsumer consumer of output from container main process
* @throws MachineException if error occurs on creation of {@code Instance}
* @param machine
* description of machine
* @param sshClient
* ssh client of machine
* @param outputConsumer
* consumer of output from container main process
* @throws MachineException
* if error occurs on creation of {@code Instance}
*/
SshMachineInstance createInstance(@Assisted Machine machine,
@Assisted SshClient sshClient,
@Assisted LineConsumer outputConsumer) throws MachineException;
public SshMachineInstance createInstance(Machine machine, SshClient sshClient, LineConsumer outputConsumer) throws MachineException {
return new SshMachineInstance(machine, sshClient, outputConsumer, this, machinesServers);
}
/**
* Creates ssh machine implementation of {@link org.eclipse.che.api.machine.server.spi.InstanceProcess}.
* Creates ssh machine implementation of {@link SshMachineProcess}.
*
* @param command command that should be executed on process start
* @param outputChannel channel where output will be available on process execution
* @param pid virtual id of that process
* @param sshClient client to communicate with machine
* @param command
* command that should be executed on process start
* @param outputChannel
* channel where output will be available on process execution
* @param pid
* virtual id of that process
* @param sshClient
* client to communicate with machine
*/
SshMachineProcess createInstanceProcess(@Assisted Command command,
@Assisted("outputChannel") String outputChannel,
@Assisted int pid,
@Assisted SshClient sshClient);
public SshMachineProcess createInstanceProcess(Command command, String outputChannel, int pid, SshClient sshClient) {
return new SshMachineProcess(command, outputChannel, pid, sshClient);
}
}

View File

@ -10,24 +10,19 @@
*******************************************************************************/
package org.eclipse.che.plugin.machine.ssh;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.model.machine.Command;
import org.eclipse.che.api.core.model.machine.Machine;
import org.eclipse.che.api.core.model.machine.MachineSource;
import org.eclipse.che.api.core.model.machine.MachineConfig;
import org.eclipse.che.api.core.model.machine.MachineStatus;
import org.eclipse.che.api.core.model.machine.ServerConf;
import org.eclipse.che.api.core.util.LineConsumer;
import org.eclipse.che.api.machine.server.exception.MachineException;
import org.eclipse.che.api.machine.server.model.impl.MachineRuntimeInfoImpl;
import org.eclipse.che.api.machine.server.model.impl.ServerImpl;
import org.eclipse.che.api.machine.server.spi.Instance;
import org.eclipse.che.api.machine.server.spi.InstanceNode;
import org.eclipse.che.api.machine.server.spi.InstanceProcess;
import org.eclipse.che.api.machine.server.spi.impl.AbstractInstance;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.URI;
@ -44,46 +39,57 @@ import static java.lang.String.format;
import static java.util.Collections.emptyMap;
/**
* Implementation of {@link Instance} that uses represents ssh machine.
* Implementation of machine that represents ssh machine.
*
* @author Alexander Garagatyi
* @author Max Shaposhnik
* @see SshMachineInstanceProvider
*/
// todo try to avoid map of processes
public class SshMachineInstance extends AbstractInstance {
public class SshMachineInstance {
private static final AtomicInteger pidSequence = new AtomicInteger(1);
private final SshClient sshClient;
private final LineConsumer outputConsumer;
private final SshMachineFactory machineFactory;
private String id;
private String workspaceId;
private final String envName;
private final String owner;
private MachineRuntimeInfoImpl machineRuntime;
private final MachineConfig machineConfig;
private final Set<ServerConf> machinesServers;
private final ConcurrentHashMap<Integer, InstanceProcess> machineProcesses;
private MachineRuntimeInfoImpl machineRuntime;
private final SshClient sshClient;
private final LineConsumer outputConsumer;
private final SshMachineFactory machineFactory;
private MachineStatus status;
@Inject
public SshMachineInstance(@Assisted Machine machine,
@Assisted SshClient sshClient,
@Assisted LineConsumer outputConsumer,
private final Set<ServerConf> machinesServers;
private final ConcurrentHashMap<Integer, SshMachineProcess> machineProcesses;
public SshMachineInstance(Machine machine,
SshClient sshClient,
LineConsumer outputConsumer,
SshMachineFactory machineFactory,
@Named("machine.ssh.machine_servers") Set<ServerConf> machinesServers) {
super(machine);
Set<ServerConf> machinesServers) {
this.id = machine.getId();
this.workspaceId = machine.getWorkspaceId();
this.envName = machine.getEnvName();
this.owner = machine.getOwner();
this.sshClient = sshClient;
this.outputConsumer = outputConsumer;
this.machineFactory = machineFactory;
this.machineConfig = machine.getConfig();
this.status = machine.getStatus();
this.machinesServers = new HashSet<>(machinesServers.size() + machine.getConfig().getServers().size());
this.machinesServers.addAll(machinesServers);
this.machinesServers.addAll(machine.getConfig().getServers());
this.machineProcesses = new ConcurrentHashMap<>();
}
@Override
public LineConsumer getLogger() {
return outputConsumer;
}
@Override
public MachineRuntimeInfoImpl getRuntime() {
// lazy initialization
if (machineRuntime == null) {
@ -103,9 +109,8 @@ public class SshMachineInstance extends AbstractInstance {
return machineRuntime;
}
@Override
public InstanceProcess getProcess(final int pid) throws NotFoundException, MachineException {
final InstanceProcess machineProcess = machineProcesses.get(pid);
public SshMachineProcess getProcess(final int pid) throws NotFoundException, MachineException {
final SshMachineProcess machineProcess = machineProcesses.get(pid);
if (machineProcess == null) {
throw new NotFoundException(format("Process with pid %s not found", pid));
}
@ -118,38 +123,26 @@ public class SshMachineInstance extends AbstractInstance {
}
}
@Override
public List<InstanceProcess> getProcesses() throws MachineException {
public List<SshMachineProcess> getProcesses() throws MachineException {
// todo get children of session process
return machineProcesses.values()
.stream()
.filter(InstanceProcess::isAlive)
.filter(SshMachineProcess::isAlive)
.collect(Collectors.toList());
}
@Override
public InstanceProcess createProcess(Command command, String outputChannel) throws MachineException {
public SshMachineProcess createProcess(Command command, String outputChannel) throws MachineException {
final Integer pid = pidSequence.getAndIncrement();
SshMachineProcess instanceProcess = machineFactory.createInstanceProcess(command, outputChannel, pid, sshClient);
SshMachineProcess machineProcess = machineFactory.createInstanceProcess(command, outputChannel, pid, sshClient);
machineProcesses.put(pid, instanceProcess);
machineProcesses.put(pid, machineProcess);
return instanceProcess;
return machineProcess;
}
/**
* Not implemented.<p/>
*
* {@inheritDoc}
*/
@Override
public MachineSource saveToSnapshot() throws MachineException {
throw new MachineException("Snapshot feature is unsupported for ssh machine implementation");
}
@Override
public void destroy() throws MachineException {
try {
outputConsumer.close();
@ -161,34 +154,23 @@ public class SshMachineInstance extends AbstractInstance {
sshClient.stop();
}
@Override
public InstanceNode getNode() {
return null;// todo
}
/**
* Not implemented.<p/>
*
* {@inheritDoc}
*/
@Override
public String readFileContent(String filePath, int startFrom, int limit) throws MachineException {
// todo
throw new MachineException("File content reading is not implemented in ssh machine implementation");
public MachineStatus getStatus() {
return status;
}
/**
* Not implemented.<p/>
*
* {@inheritDoc}
*/
@Override
public void copy(Instance sourceMachine, String sourcePath, String targetPath, boolean overwrite) throws MachineException {
//todo
throw new MachineException("Copying is not implemented in ssh machine implementation");
public void setStatus(MachineStatus status) {
this.status = status;
}
public String getId() {
return id;
}
@Override
public void copy(String sourcePath, String targetPath) throws MachineException {
sshClient.copy(sourcePath, targetPath);
}
@ -208,4 +190,19 @@ public class SshMachineInstance extends AbstractInstance {
null);
}
public String getWorkspaceId() {
return workspaceId;
}
public MachineConfig getMachineConfig() {
return machineConfig;
}
public String getEnvName() {
return envName;
}
public String getOwner() {
return owner;
}
}

View File

@ -21,10 +21,7 @@ import org.eclipse.che.api.core.model.machine.Recipe;
import org.eclipse.che.api.core.util.LineConsumer;
import org.eclipse.che.api.machine.server.exception.InvalidRecipeException;
import org.eclipse.che.api.machine.server.exception.MachineException;
import org.eclipse.che.api.machine.server.exception.SnapshotException;
import org.eclipse.che.api.machine.server.exception.UnsupportedRecipeException;
import org.eclipse.che.api.machine.server.spi.Instance;
import org.eclipse.che.api.machine.server.spi.InstanceProvider;
import org.eclipse.che.api.machine.server.util.RecipeDownloader;
import javax.inject.Inject;
@ -35,7 +32,7 @@ import java.util.Set;
import static java.util.Objects.requireNonNull;
/**
* Implementation of {@link InstanceProvider} based on communication with machine over ssh protocol.
* Instance provider based on communication with machine over ssh protocol.
*
* <p>Ssh machine can't be actually created and exists somewhere outside of the control.<br>
* So this implementation just performs command execution in such machines.<br>
@ -44,7 +41,7 @@ import static java.util.Objects.requireNonNull;
* @author Alexander Garagatyi
*/
// todo tests
public class SshMachineInstanceProvider implements InstanceProvider {
public class SshMachineInstanceProvider {
private static final Gson GSON = new Gson();
private final Set<String> supportedRecipeTypes;
@ -58,12 +55,10 @@ public class SshMachineInstanceProvider implements InstanceProvider {
this.supportedRecipeTypes = Collections.singleton("ssh-config");
}
@Override
public String getType() {
return "ssh";
}
@Override
public Set<String> getRecipeTypes() {
return supportedRecipeTypes;
}
@ -76,7 +71,7 @@ public class SshMachineInstanceProvider implements InstanceProvider {
* machine description
* @param lineConsumer
* output for instance creation logs
* @return newly created {@link Instance}
* @return newly created {@link SshMachineInstance}
* @throws UnsupportedRecipeException
* if specified {@code recipe} is not supported
* @throws InvalidRecipeException
@ -86,9 +81,7 @@ public class SshMachineInstanceProvider implements InstanceProvider {
* @throws MachineException
* if other error occurs
*/
@Override
public Instance createInstance(Machine machine, LineConsumer lineConsumer)
throws UnsupportedRecipeException, InvalidRecipeException, NotFoundException, MachineException {
public SshMachineInstance createInstance(Machine machine, LineConsumer lineConsumer) throws NotFoundException, MachineException {
requireNonNull(machine, "Non null machine required");
requireNonNull(lineConsumer, "Non null logs consumer required");
requireNonNull(machine.getConfig().getSource().getLocation(), "Location in machine source is required");
@ -111,18 +104,4 @@ public class SshMachineInstanceProvider implements InstanceProvider {
instance.setStatus(MachineStatus.RUNNING);
return instance;
}
/**
* Removes snapshot of the instance in implementation specific way.
*
* @param machineSource
* contains implementation specific key of the snapshot of the instance that should be removed
* @throws SnapshotException
* if exception occurs on instance snapshot removal
*/
@Override
public void removeInstanceSnapshot(MachineSource machineSource) throws SnapshotException {
throw new SnapshotException("Snapshot feature is unsupported for ssh machine implementation");
}
}

View File

@ -11,7 +11,6 @@
package org.eclipse.che.plugin.machine.ssh;
import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
@ -23,20 +22,9 @@ import com.google.inject.name.Names;
public class SshMachineModule extends AbstractModule {
@Override
protected void configure() {
Multibinder<org.eclipse.che.api.machine.server.spi.InstanceProvider> machineProviderMultibinder =
Multibinder.newSetBinder(binder(),
org.eclipse.che.api.machine.server.spi.InstanceProvider.class);
machineProviderMultibinder.addBinding()
.to(SshMachineInstanceProvider.class);
bind(SshMachineInstanceProvider.class);
install(new FactoryModuleBuilder()
.implement(org.eclipse.che.api.machine.server.spi.Instance.class,
org.eclipse.che.plugin.machine.ssh.SshMachineInstance.class)
.implement(org.eclipse.che.api.machine.server.spi.InstanceProcess.class,
org.eclipse.che.plugin.machine.ssh.SshMachineProcess.class)
.implement(org.eclipse.che.plugin.machine.ssh.SshClient.class,
org.eclipse.che.plugin.machine.ssh.jsch.JschSshClient.class)
.build(SshMachineFactory.class));
bind(SshMachineFactory.class);
bindConstant().annotatedWith(Names.named("machine.ssh.server.terminal.location")).to("~/che");

View File

@ -17,24 +17,28 @@ import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.model.machine.Command;
import org.eclipse.che.api.core.util.LineConsumer;
import org.eclipse.che.api.machine.server.exception.MachineException;
import org.eclipse.che.api.machine.server.spi.InstanceProcess;
import org.eclipse.che.api.machine.server.spi.impl.AbstractMachineProcess;
import org.eclipse.che.commons.annotation.Nullable;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Map;
import static java.lang.String.format;
/**
* Ssh machine implementation of {@link InstanceProcess}
* Ssh machine process implementation.
*
* @author Alexander Garagatyi
*/
public class SshMachineProcess extends AbstractMachineProcess implements InstanceProcess {
private final String commandLine;
private final SshClient sshClient;
public class SshMachineProcess {
private final SshClient sshClient;
private final String name;
private final String commandLine;
private final String type;
private final Map<String, String> attributes;
private final int pid;
private final String outputChannel;
private volatile boolean started;
@ -45,13 +49,16 @@ public class SshMachineProcess extends AbstractMachineProcess implements Instanc
@Nullable @Assisted("outputChannel") String outputChannel,
@Assisted int pid,
@Assisted SshClient sshClient) {
super(command, pid, outputChannel);
this.sshClient = sshClient;
this.commandLine = command.getCommandLine();
this.started = false;
this.name = command.getName();
this.type = command.getType();
this.attributes = command.getAttributes();
this.pid = pid;
this.outputChannel = outputChannel;
}
@Override
public boolean isAlive() {
if (!started) {
return false;
@ -66,12 +73,10 @@ public class SshMachineProcess extends AbstractMachineProcess implements Instanc
}
}
@Override
public void start() throws ConflictException, MachineException {
start(null);
}
@Override
public void start(LineConsumer output) throws ConflictException, MachineException {
if (started) {
throw new ConflictException("Process already started.");
@ -89,22 +94,45 @@ public class SshMachineProcess extends AbstractMachineProcess implements Instanc
}
}
@Override
public void checkAlive() throws MachineException, NotFoundException {
if (!started) {
throw new NotFoundException("Process is not started yet");
}
if (sshProcess.getExitCode() != -1) {
throw new NotFoundException(format("Process with pid %s not found", getPid()));
throw new NotFoundException(format("Process with pid %s not found", pid));
}
}
@Override
public void kill() throws MachineException {
sshProcess.kill();
}
public String getName() {
return name;
}
public String getType() {
return type;
}
public int getPid() {
return pid;
}
public String getCommandLine() {
return commandLine;
}
public Map<String, String> getAttributes() {
return attributes;
}
public String getOutputChannel() {
return outputChannel;
}
private static class PrefixingLineConsumer implements LineConsumer {
private final String prefix;
private final LineConsumer lineConsumer;

View File

@ -0,0 +1,215 @@
/*******************************************************************************
* 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;
}
}

View File

@ -0,0 +1,56 @@
/*******************************************************************************
* 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 org.eclipse.che.api.agent.shared.model.Agent;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.model.machine.Command;
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.plugin.machine.ssh.SshMachineInstance;
import org.eclipse.che.plugin.machine.ssh.SshMachineProcess;
import static java.lang.String.format;
/**
* Verifies if a specific process is started on machine.
*
* @author Max Shaposhnik (mshaposhnik@codenvy.com)
*/
public class SshProcessLaunchedChecker {
private static final String CHECK_COMMAND = "command -v pidof >/dev/null 2>&1 && {\n" +
" pidof %1$s >/dev/null 2>&1 && echo 0 || echo 1\n" +
"} || {\n" +
" ps -fC %1$s >/dev/null 2>&1 && echo 0 || echo 1\n" +
"}";
private final String processNameToWait;
private long counter;
public SshProcessLaunchedChecker(String processNameToWait) {
this.processNameToWait = processNameToWait;
}
public boolean isLaunched(Agent agent, SshMachineInstance machine) throws MachineException {
Command command = new CommandImpl(format("Wait for %s, try %d", agent.getId(), ++counter),
format(CHECK_COMMAND, processNameToWait),
"test");
try (ListLineConsumer lineConsumer = new ListLineConsumer()) {
SshMachineProcess waitProcess = machine.createProcess(command, null);
waitProcess.start(lineConsumer);
return lineConsumer.getText().endsWith("[STDOUT] 0");
} catch (ConflictException e) {
throw new MachineException(e.getServiceError());
}
}
}

View File

@ -10,7 +10,6 @@
*******************************************************************************/
package org.eclipse.che.plugin.machine.ssh.jsch;
import com.google.inject.assistedinject.Assisted;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
@ -60,8 +59,8 @@ public class JschSshClient implements SshClient {
private Session session;
@Inject
public JschSshClient(@Assisted SshMachineRecipe sshMachineRecipe,
@Assisted Map<String, String> envVars,
public JschSshClient(SshMachineRecipe sshMachineRecipe,
Map<String, String> envVars,
JSch jsch,
@Named("che.workspace.ssh_connection_timeout_ms") int connectionTimeoutMs) {
this.envVars = envVars;

View File

@ -17,13 +17,11 @@ import org.eclipse.che.api.core.model.machine.MachineConfig;
import org.eclipse.che.api.core.model.machine.MachineStatus;
import org.eclipse.che.api.core.util.LineConsumer;
import org.eclipse.che.api.machine.server.exception.MachineException;
import org.eclipse.che.api.machine.server.exception.SnapshotException;
import org.eclipse.che.api.machine.server.model.impl.MachineConfigImpl;
import org.eclipse.che.api.machine.server.model.impl.MachineImpl;
import org.eclipse.che.api.machine.server.model.impl.MachineSourceImpl;
import org.eclipse.che.api.machine.server.model.impl.ServerConfImpl;
import org.eclipse.che.api.machine.server.recipe.RecipeImpl;
import org.eclipse.che.api.machine.server.spi.Instance;
import org.eclipse.che.api.machine.server.util.RecipeDownloader;
import org.mockito.InjectMocks;
import org.mockito.Mock;
@ -67,10 +65,6 @@ public class SshMachineInstanceProviderTest {
@BeforeMethod
public void setUp() throws Exception {
machine = createMachine();
SshMachineRecipe sshMachineRecipe = new SshMachineRecipe("localhost",
22,
"user",
"password");
recipe = new RecipeImpl().withType("ssh-config")
.withScript(RECIPE_SCRIPT);
}
@ -85,12 +79,6 @@ public class SshMachineInstanceProviderTest {
assertEquals(provider.getRecipeTypes(), new HashSet<>(singletonList("ssh-config")));
}
@Test(expectedExceptions = SnapshotException.class,
expectedExceptionsMessageRegExp = "Snapshot feature is unsupported for ssh machine implementation")
public void shouldThrowSnapshotExceptionOnRemoveSnapshot() throws Exception {
provider.removeInstanceSnapshot(null);
}
@Test(expectedExceptions = MachineException.class,
expectedExceptionsMessageRegExp = "Dev machine is not supported for Ssh machine implementation")
public void shouldThrowExceptionOnDevMachineCreationFromRecipe() throws Exception {
@ -114,7 +102,7 @@ public class SshMachineInstanceProviderTest {
when(sshMachineFactory.createInstance(eq(machine), eq(sshClient), any(LineConsumer.class))).thenReturn(sshMachineInstance);
when(recipeDownloader.getRecipe(eq(machine.getConfig()))).thenReturn(recipe);
Instance instance = provider.createInstance(machine, LineConsumer.DEV_NULL);
SshMachineInstance instance = provider.createInstance(machine, LineConsumer.DEV_NULL);
assertEquals(instance, sshMachineInstance);
}