From 3f2e0d6f756ddf9cbbbb6687704c337d5e5f0767 Mon Sep 17 00:00:00 2001 From: Alexander Garagatyi Date: Mon, 27 Mar 2017 12:27:27 +0300 Subject: [PATCH] CHE-4098: separate terminal and exec agents (#4486) Refactor golang terminal code. Add ping frames into terminal websocket connection. Signed-off-by: Alexander Garagatyi --- agents/exec/pom.xml | 192 +++++++++ agents/exec/src/assembly/assembly.xml | 33 ++ .../org/eclipse/che/api/agent/ExecAgent.java | 4 +- .../che/api/agent/ExecAgentLauncher.java | 6 +- .../main/resources/org.eclipse.che.exec.json | 13 + .../resources/org.eclipse.che.exec.script.sh | 175 ++++++++ agents/go-agents/.gitignore | 1 + agents/go-agents/README.md | 6 + agents/go-agents/pom.xml | 159 +------- .../src/main/go/core/activity/activity.go | 48 ++- .../src/main/go/core/activity/noop.go | 23 ++ .../src/main/go/core/common/error.go | 22 ++ .../src/main/go/core/process/file_logger.go | 10 +- .../main/go/core/process/file_logger_test.go | 13 +- .../src/main/go/core/process/logs_reader.go | 2 +- .../main/go/core/process/logs_reader_test.go | 2 +- .../src/main/go/core/rest/default_router.go | 100 +++++ .../go-agents/src/main/go/core/rest/errors.go | 18 +- .../go-agents/src/main/go/core/rest/route.go | 2 +- .../go-agents/src/main/go/core/rest/router.go | 34 ++ .../src/main/go/core/rpc/channels.go | 17 + .../go-agents/src/main/go/core/rpc/route.go | 30 +- .../exec-agent/exec/logs_distributor_test.go | 11 +- .../src/main/go/exec-agent/exec/process.go | 107 +++-- .../exec-agent/exec/process_cleaner_test.go | 2 +- .../main/go/exec-agent/exec/process_test.go | 17 +- .../main/go/exec-agent/exec/rest_service.go | 78 ++-- .../go-agents/src/main/go/exec-agent/main.go | 207 +++------- .../src/main/go/exec-agent/term/server.go | 373 ------------------ .../src/main/go/terminal-agent/main.go | 171 ++++++++ .../main/go/terminal-agent/term/finalizer.go | 101 +++++ .../src/main/go/terminal-agent/term/pty.go | 142 +++++++ .../src/main/go/terminal-agent/term/server.go | 179 +++++++++ .../main/go/terminal-agent/term/websocket.go | 45 +++ agents/pom.xml | 3 +- agents/terminal/pom.xml | 192 +++++++++ .../src/assembly/assembly.xml | 0 .../eclipse/che/api/agent/TerminalAgent.java | 37 ++ .../che/api/agent/TerminalAgentLauncher.java | 57 +++ .../resources/org.eclipse.che.terminal.json | 0 .../org.eclipse.che.terminal.script.sh | 0 assembly/assembly-main/pom.xml | 6 +- .../assembly-main/src/assembly/assembly.xml | 8 +- .../src/assembly/stack/che-in-che.json | 2 +- assembly/assembly-wsmaster-war/pom.xml | 6 +- .../che/api/deploy/WsMasterModule.java | 13 +- .../WEB-INF/classes/codenvy/che.properties | 2 + .../e2e/stacks/list-stacks/list-stack.mock.js | 2 +- .../stack-details/stack-details.mock.js | 6 +- .../add-machine-dialog.controller.ts | 2 +- .../src/components/api/che-stack.factory.ts | 2 +- .../components/api/che-workspace.factory.ts | 4 +- dockerfiles/che/entrypoint.sh | 1 + .../src/api/wsmaster/workspace/workspace.ts | 1 + .../action/impl/execute-command-action.ts | 4 +- dockerfiles/lib/src/internal/dir/che-dir.ts | 6 +- .../src/main/resources/stacks.json | 64 +-- .../ext/DockerMachineTerminalChecker.java | 1 + .../ext/provider/ExecAgentVolumeProvider.java | 80 ++++ .../LocalCheInfrastructureProvisioner.java | 11 +- .../openshift/client/CheServicePorts.java | 4 +- .../kubernetes/KubernetesServiceTest.java | 13 +- plugins/plugin-terminal-ui/pom.xml | 44 +-- .../che-plugin-testing-junit-server/pom.xml | 4 +- pom.xml | 25 +- .../resources/org.eclipse.che.ws-agent.json | 1 + .../api/factory/server/FactoryService.java | 2 + .../factory/server/FactoryServiceTest.java | 66 ++++ wsmaster/che-core-api-workspace/pom.xml | 9 +- .../AddExecAgentInEnvironmentUtil.java | 47 +++ .../filters/AddExecAgentInStackFilter.java | 41 ++ .../AddExecAgentInWorkspaceFilter.java | 42 ++ .../server/MachineLinksInjector.java | 2 +- .../server/WorkspaceConfigJsonAdapter.java | 1 + .../workspace/server/WorkspaceRuntimes.java | 3 +- .../server/WorkspaceServiceLinksInjector.java | 8 +- .../AddExecAgentInStackFilterTest.java | 238 +++++++++++ .../AddExecAgentInWorkspaceFilterTest.java | 254 ++++++++++++ .../server/WorkspaceRuntimesTest.java | 6 +- ..._agent_where_terminal_agent_is_present.sql | 16 + 80 files changed, 2790 insertions(+), 889 deletions(-) create mode 100644 agents/exec/pom.xml create mode 100644 agents/exec/src/assembly/assembly.xml rename agents/{go-agents => exec}/src/main/java/org/eclipse/che/api/agent/ExecAgent.java (96%) rename agents/{go-agents => exec}/src/main/java/org/eclipse/che/api/agent/ExecAgentLauncher.java (91%) create mode 100644 agents/exec/src/main/resources/org.eclipse.che.exec.json create mode 100644 agents/exec/src/main/resources/org.eclipse.che.exec.script.sh create mode 100644 agents/go-agents/src/main/go/core/activity/noop.go create mode 100644 agents/go-agents/src/main/go/core/common/error.go create mode 100644 agents/go-agents/src/main/go/core/rest/default_router.go create mode 100644 agents/go-agents/src/main/go/core/rest/router.go delete mode 100644 agents/go-agents/src/main/go/exec-agent/term/server.go create mode 100644 agents/go-agents/src/main/go/terminal-agent/main.go create mode 100644 agents/go-agents/src/main/go/terminal-agent/term/finalizer.go create mode 100644 agents/go-agents/src/main/go/terminal-agent/term/pty.go create mode 100644 agents/go-agents/src/main/go/terminal-agent/term/server.go create mode 100644 agents/go-agents/src/main/go/terminal-agent/term/websocket.go create mode 100644 agents/terminal/pom.xml rename agents/{go-agents => terminal}/src/assembly/assembly.xml (100%) create mode 100644 agents/terminal/src/main/java/org/eclipse/che/api/agent/TerminalAgent.java create mode 100644 agents/terminal/src/main/java/org/eclipse/che/api/agent/TerminalAgentLauncher.java rename agents/{go-agents => terminal}/src/main/resources/org.eclipse.che.terminal.json (100%) rename agents/{go-agents => terminal}/src/main/resources/org.eclipse.che.terminal.script.sh (100%) create mode 100644 plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ext/provider/ExecAgentVolumeProvider.java create mode 100644 wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInEnvironmentUtil.java create mode 100644 wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInStackFilter.java create mode 100644 wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInWorkspaceFilter.java create mode 100644 wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInStackFilterTest.java create mode 100644 wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInWorkspaceFilterTest.java create mode 100644 wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.6.0/1__add_exec_agent_where_terminal_agent_is_present.sql diff --git a/agents/exec/pom.xml b/agents/exec/pom.xml new file mode 100644 index 00000000000..fa338553660 --- /dev/null +++ b/agents/exec/pom.xml @@ -0,0 +1,192 @@ + + + + 4.0.0 + + che-agents-parent + org.eclipse.che + 5.6.0-SNAPSHOT + + exec-agent + Agent :: Exec + + + com.google.inject + guice + + + javax.inject + javax.inject + + + org.eclipse.che.core + che-core-api-agent + + + org.eclipse.che.core + che-core-api-agent-shared + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-machine + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + copy-sources + compile + + run + + + + + + + + + + + + + + + + + com.soebes.maven.plugins + iterator-maven-plugin + 0.5.0 + + + compile-go-agents + compile + + iterator + + + + + linux_arm5 + + linux + arm + 5 + + + + linux_arm6 + + linux + arm + 6 + + + + linux_arm7 + + linux + arm + 7 + + + + linux_amd64 + + linux + amd64 + + + + linux_i386 + + linux + 386 + + + + + + + org.codehaus.mojo + exec-maven-plugin + + exec + + go + ${project.build.directory}/go-workspace/src/github.com/eclipse/che/agents/go-agents/src/main/go/exec-agent + + build + -a + -installsuffix + cgo + -o + ${project.build.directory}/${item}/che-exec-agent + + + 0 + ${project.build.directory}/go-workspace + ${go.target.os} + ${go.target.architecture} + ${go.target.arm.version} + + + + + + + + assembly + package + + iterator + + + + linux_arm5 + linux_arm6 + linux_arm7 + linux_amd64 + linux_i386 + + + + + maven-assembly-plugin + + single + + posix + + ${basedir}/src/assembly/assembly.xml + + + + + + + + + + + diff --git a/agents/exec/src/assembly/assembly.xml b/agents/exec/src/assembly/assembly.xml new file mode 100644 index 00000000000..6d31c94e686 --- /dev/null +++ b/agents/exec/src/assembly/assembly.xml @@ -0,0 +1,33 @@ + + + ${item} + true + exec-agent + + zip + tar.gz + + + + ${project.build.directory}/${item} + + che-exec-agent + + 0755 + + + + diff --git a/agents/go-agents/src/main/java/org/eclipse/che/api/agent/ExecAgent.java b/agents/exec/src/main/java/org/eclipse/che/api/agent/ExecAgent.java similarity index 96% rename from agents/go-agents/src/main/java/org/eclipse/che/api/agent/ExecAgent.java rename to agents/exec/src/main/java/org/eclipse/che/api/agent/ExecAgent.java index 78a18d74879..8808f01a945 100644 --- a/agents/go-agents/src/main/java/org/eclipse/che/api/agent/ExecAgent.java +++ b/agents/exec/src/main/java/org/eclipse/che/api/agent/ExecAgent.java @@ -27,8 +27,8 @@ */ @Singleton public class ExecAgent extends BasicAgent { - private static final String AGENT_DESCRIPTOR = "org.eclipse.che.terminal.json"; - private static final String AGENT_SCRIPT = "org.eclipse.che.terminal.script.sh"; + private static final String AGENT_DESCRIPTOR = "org.eclipse.che.exec.json"; + private static final String AGENT_SCRIPT = "org.eclipse.che.exec.script.sh"; @Inject public ExecAgent() throws IOException { diff --git a/agents/go-agents/src/main/java/org/eclipse/che/api/agent/ExecAgentLauncher.java b/agents/exec/src/main/java/org/eclipse/che/api/agent/ExecAgentLauncher.java similarity index 91% rename from agents/go-agents/src/main/java/org/eclipse/che/api/agent/ExecAgentLauncher.java rename to agents/exec/src/main/java/org/eclipse/che/api/agent/ExecAgentLauncher.java index 2ce114572a5..3e8fab595f3 100644 --- a/agents/go-agents/src/main/java/org/eclipse/che/api/agent/ExecAgentLauncher.java +++ b/agents/exec/src/main/java/org/eclipse/che/api/agent/ExecAgentLauncher.java @@ -33,8 +33,8 @@ public class ExecAgentLauncher extends AbstractAgentLauncher { @Inject public ExecAgentLauncher(@Named("che.agent.dev.max_start_time_ms") long agentMaxStartTimeMs, @Named("che.agent.dev.ping_delay_ms") long agentPingDelayMs, - @Named("machine.terminal_agent.run_command") String runCommand) { - super(agentMaxStartTimeMs, agentPingDelayMs, new ProcessIsLaunchedChecker("che-websocket-terminal")); + @Named("machine.exec_agent.run_command") String runCommand) { + super(agentMaxStartTimeMs, agentPingDelayMs, new ProcessIsLaunchedChecker("che-exec-agent")); this.runCommand = runCommand; } @@ -52,6 +52,6 @@ public String getMachineType() { @Override public String getAgentId() { - return "org.eclipse.che.terminal"; + return "org.eclipse.che.exec"; } } diff --git a/agents/exec/src/main/resources/org.eclipse.che.exec.json b/agents/exec/src/main/resources/org.eclipse.che.exec.json new file mode 100644 index 00000000000..58d3d59ddce --- /dev/null +++ b/agents/exec/src/main/resources/org.eclipse.che.exec.json @@ -0,0 +1,13 @@ +{ + "id": "org.eclipse.che.exec", + "name": "Exec-agent", + "description": "Agent for command execution", + "dependencies": [], + "properties": {}, + "servers": { + "exec-agent": { + "port": "4412/tcp", + "protocol": "http" + } + } +} diff --git a/agents/exec/src/main/resources/org.eclipse.che.exec.script.sh b/agents/exec/src/main/resources/org.eclipse.che.exec.script.sh new file mode 100644 index 00000000000..6cf6047c6f6 --- /dev/null +++ b/agents/exec/src/main/resources/org.eclipse.che.exec.script.sh @@ -0,0 +1,175 @@ +# +# 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 +# + +unset PACKAGES +unset SUDO +command -v tar >/dev/null 2>&1 || { PACKAGES=${PACKAGES}" tar"; } +command -v curl >/dev/null 2>&1 || { PACKAGES=${PACKAGES}" curl"; } +test "$(id -u)" = 0 || SUDO="sudo -E" + +CHE_DIR=$HOME/che +LOCAL_AGENT_BINARIES_URI='/mnt/che/exec-agent/exec-agent-${PREFIX}.tar.gz' +DOWNLOAD_AGENT_BINARIES_URI='${WORKSPACE_MASTER_URI}/agent-binaries/${PREFIX}/exec/exec-agent-${PREFIX}.tar.gz' +TARGET_AGENT_BINARIES_URI='file://${CHE_DIR}/exec-agent-${PREFIX}.tar.gz' + +if [ -f /etc/centos-release ]; then + FILE="/etc/centos-release" + LINUX_TYPE=$(cat $FILE | awk '{print $1}') + elif [ -f /etc/redhat-release ]; then + FILE="/etc/redhat-release" + LINUX_TYPE=$(cat $FILE | cut -c 1-8) + else + FILE="/etc/os-release" + LINUX_TYPE=$(cat $FILE | grep ^ID= | tr '[:upper:]' '[:lower:]') + LINUX_VERSION=$(cat $FILE | grep ^VERSION_ID=) +fi +MACHINE_TYPE=$(uname -m) +SHELL_INTERPRETER="/bin/sh" + +mkdir -p ${CHE_DIR} + +######################## +### Install packages ### +######################## + +# Red Hat Enterprise Linux 7 +############################ +if echo ${LINUX_TYPE} | grep -qi "rhel"; then + test "${PACKAGES}" = "" || { + ${SUDO} yum install ${PACKAGES}; + } + +# Ubuntu 14.04 16.04 / Linux Mint 17 +#################################### +elif echo ${LINUX_TYPE} | grep -qi "ubuntu"; then + test "${PACKAGES}" = "" || { + ${SUDO} apt-get update; + ${SUDO} apt-get -y install ${PACKAGES}; + } + +# Debian 8 +########## +elif echo ${LINUX_TYPE} | grep -qi "debian"; then + test "${PACKAGES}" = "" || { + ${SUDO} apt-get update; + ${SUDO} apt-get -y install ${PACKAGES}; + } + +# Fedora 23 +########### +elif echo ${LINUX_TYPE} | grep -qi "fedora"; then + command -v ps >/dev/null 2>&1 || { PACKAGES=${PACKAGES}" procps-ng"; } + test "${PACKAGES}" = "" || { + ${SUDO} dnf -y install ${PACKAGES}; + } + +# CentOS 7.1 & Oracle Linux 7.1 +############################### +elif echo ${LINUX_TYPE} | grep -qi "centos"; then + test "${PACKAGES}" = "" || { + ${SUDO} yum -y install ${PACKAGES}; + } + +# openSUSE 13.2 +############### +elif echo ${LINUX_TYPE} | grep -qi "opensuse"; then + test "${PACKAGES}" = "" || { + ${SUDO} zypper install -y ${PACKAGES}; + } + +# Alpine 3.3 +############ +elif echo ${LINUX_TYPE} | grep -qi "alpine"; then + test "${PACKAGES}" = "" || { + ${SUDO} apk update + ${SUDO} apk add ${PACKAGES}; + } + +# Centos 6.6, 6.7, 6.8 +############ +elif echo ${LINUX_TYPE} | grep -qi "CentOS"; then + test "${PACKAGES}" = "" || { + ${SUDO} yum -y install ${PACKAGES}; + } + +# Red Hat Enterprise Linux 6 +############################ + +elif echo ${LINUX_TYPE} | grep -qi "Red Hat"; then + test "${PACKAGES}" = "" || { + ${SUDO} yum install ${PACKAGES}; + } + +else + >&2 echo "Unrecognized Linux Type" + >&2 cat $FILE + exit 1 +fi + +command -v pidof >/dev/null 2>&1 && { + pidof exec-agent >/dev/null 2>&1 && exit +} || { + ps -fC exec-agent >/dev/null 2>&1 && exit +} + + +######################## +### Install Exec agent ### +######################## +if echo ${MACHINE_TYPE} | grep -qi "x86_64"; then + PREFIX=linux_amd64 +elif echo ${MACHINE_TYPE} | grep -qi "arm5"; then + PREFIX=linux_arm7 +elif echo ${MACHINE_TYPE} | grep -qi "arm6"; then + PREFIX=linux_arm7 +elif echo ${MACHINE_TYPE} | grep -qi "arm7"; then + PREFIX=linux_arm7 +elif echo ${MACHINE_TYPE} | grep -qi "armv7l"; then + PREFIX=linux_arm7 +else + >&2 echo "Unrecognized Machine Type" + >&2 uname -a + exit 1 +fi + +# Compute URI of workspace master +WORKSPACE_MASTER_URI=$(echo $CHE_API | cut -d / -f 1-3) + +## Evaluate variables now that prefix is defined +eval "LOCAL_AGENT_BINARIES_URI=${LOCAL_AGENT_BINARIES_URI}" +eval "DOWNLOAD_AGENT_BINARIES_URI=${DOWNLOAD_AGENT_BINARIES_URI}" +eval "TARGET_AGENT_BINARIES_URI=${TARGET_AGENT_BINARIES_URI}" + +if [ -f "${LOCAL_AGENT_BINARIES_URI}" ]; then + AGENT_BINARIES_URI="file://${LOCAL_AGENT_BINARIES_URI}" +elif [ -f $(echo "${LOCAL_AGENT_BINARIES_URI}" | sed "s/-${PREFIX}//g") ]; then + AGENT_BINARIES_URI="file://"$(echo "${LOCAL_AGENT_BINARIES_URI}" | sed "s/-${PREFIX}//g") +else + echo "Exec Agent will be downloaded from Workspace Master" + AGENT_BINARIES_URI=${DOWNLOAD_AGENT_BINARIES_URI} +fi + + +if curl -o /dev/null --silent --head --fail $(echo ${AGENT_BINARIES_URI} | sed 's/\${PREFIX}/'${PREFIX}'/g'); then + curl -o $(echo ${TARGET_AGENT_BINARIES_URI} | sed 's/\${PREFIX}/'${PREFIX}'/g' | sed 's/file:\/\///g') -s $(echo ${AGENT_BINARIES_URI} | sed 's/\${PREFIX}/'${PREFIX}'/g') +elif curl -o /dev/null --silent --head --fail $(echo ${AGENT_BINARIES_URI} | sed 's/-\${PREFIX}//g'); then + curl -o $(echo ${TARGET_AGENT_BINARIES_URI} | sed 's/\${PREFIX}/'${PREFIX}'/g' | sed 's/file:\/\///g') -s $(echo ${AGENT_BINARIES_URI} | sed 's/-\${PREFIX}//g') +fi + +curl -s $(echo ${TARGET_AGENT_BINARIES_URI} | sed 's/\${PREFIX}/'${PREFIX}'/g') | tar xzf - -C ${CHE_DIR} + +if [ -f /bin/bash ]; then + SHELL_INTERPRETER="/bin/bash" +fi + +##################################################### +### exec-agent run command will be added here ### +##################################################### diff --git a/agents/go-agents/.gitignore b/agents/go-agents/.gitignore index e7f04f5c058..fd852882100 100644 --- a/agents/go-agents/.gitignore +++ b/agents/go-agents/.gitignore @@ -1,2 +1,3 @@ src/main/go/exec-agent/exec-agent src/main/go/exec-agent/logs +src/main/go/terminal-agent/terminal-agent diff --git a/agents/go-agents/README.md b/agents/go-agents/README.md index 9d7be18ade8..2a78a8f9529 100644 --- a/agents/go-agents/README.md +++ b/agents/go-agents/README.md @@ -40,6 +40,12 @@ cd $GOPATH/src/github.com/eclipse/che/agents/go-agents/src/main/go && go build . cd $GOPATH/src/github.com/eclipse/che/agents/go-agents/src/main/go/exec-agent && go build ``` +#### Building terminal agent executable + +```bash +cd $GOPATH/src/github.com/eclipse/che/agents/go-agents/src/main/go/terminal-agent && go build +``` + ##### Running linked project tests ```bash diff --git a/agents/go-agents/pom.xml b/agents/go-agents/pom.xml index 7b65efd7aa2..446b35546a0 100644 --- a/agents/go-agents/pom.xml +++ b/agents/go-agents/pom.xml @@ -20,35 +20,6 @@ go-agents Agent :: Golang agents - - go-workspace - - - - com.google.inject - guice - - - javax.inject - javax.inject - - - org.eclipse.che.core - che-core-api-agent - - - org.eclipse.che.core - che-core-api-agent-shared - - - org.eclipse.che.core - che-core-api-core - - - org.eclipse.che.core - che-core-api-machine - - @@ -57,7 +28,7 @@ /docs/** - src/main/go/exec-agent/term/server.go + src/main/go/terminal-agent/term/server.go src/main/go/vendor/** src/main/go/Godeps/** @@ -77,11 +48,11 @@ - + - - + + @@ -89,115 +60,27 @@ - com.soebes.maven.plugins - iterator-maven-plugin - 0.5.0 + org.codehaus.mojo + exec-maven-plugin - compile-go-agents + build-go-sources compile - iterator + exec - - - linux_arm5 - - linux - arm - 5 - - - - linux_arm6 - - linux - arm - 6 - - - - linux_arm7 - - linux - arm - 7 - - - - linux_amd64 - - linux - amd64 - - - - linux_i386 - - linux - 386 - - - - - - - org.codehaus.mojo - exec-maven-plugin - - exec - - go - ${project.build.directory}/${go.workspace.name}/src/github.com/eclipse/che/agents/go-agents/src/main/go/exec-agent - - build - -a - -installsuffix - cgo - -o - ${project.build.directory}/${item}/che-websocket-terminal - - - 0 - ${project.build.directory}/${go.workspace.name} - ${terminal.target.os} - ${terminal.target.architecture} - ${terminal.target.arm.version} - - - - - - - - assembly - package - - iterator - - - - linux_arm5 - linux_arm6 - linux_arm7 - linux_amd64 - linux_i386 - - - - - maven-assembly-plugin - - single - - posix - - ${basedir}/src/assembly/assembly.xml - - - - + go + ${project.build.directory}/go-workspace/src/github.com/eclipse/che/agents/go-agents/src/main/go + + build + -a + ./... + + + 0 + ${project.build.directory}/go-workspace + @@ -224,8 +107,8 @@ - - + + diff --git a/agents/go-agents/src/main/go/core/activity/activity.go b/agents/go-agents/src/main/go/core/activity/activity.go index 241cddcb74d..68c7e5f0fa1 100644 --- a/agents/go-agents/src/main/go/core/activity/activity.go +++ b/agents/go-agents/src/main/go/core/activity/activity.go @@ -14,7 +14,6 @@ package activity import ( "log" "net/http" - "os" "time" ) @@ -24,46 +23,59 @@ const threshold int64 = 60 var ( // ActivityTrackingEnabled defines whether activity tracking should be used ActivityTrackingEnabled = false - // Tracker provides activity notification API - Tracker = &WorkspaceActivityTracker{} - workspaceID = os.Getenv("CHE_WORKSPACE_ID") - // APIEndpoint points to url of workspace master server - APIEndpoint string + // Tracker provides workspace activity notification client + Tracker WorkspaceActivityTracker = &NoOpActivityTracker{} ) // WorkspaceActivityTracker provides workspace activity notification API -type WorkspaceActivityTracker struct { +type WorkspaceActivityTracker interface { + Notify() + StartTracking() +} + +// Default impl of WorkspaceActivityTracker +type tracker struct { + WorkspaceActivityTracker + active bool lastUpdateTime int64 + activityAPI string +} + +// NewTracker creates default implementation of activity tracker +func NewTracker(wsID string, apiEndpoint string) WorkspaceActivityTracker { + return &tracker{ + activityAPI: apiEndpoint + "/activity/" + wsID, + } } // Notify ensures that workspace master knows about recent activity of a workspace -func (wa *WorkspaceActivityTracker) Notify() { +func (tr *tracker) Notify() { t := time.Now().Unix() - if t < (wa.lastUpdateTime + threshold) { - wa.active = true + if t < (tr.lastUpdateTime + threshold) { + tr.active = true } else { - go makeActivityRequest() - wa.lastUpdateTime = t + go tr.makeActivityRequest() + tr.lastUpdateTime = t } } // StartTracking runs scheduler that continiously notifies workspace master // if workspace activity was submitted with function Notify. // Since it is synchronious function it should be started at separate thread. -func (wa *WorkspaceActivityTracker) StartTracking() { +func (tr *tracker) StartTracking() { ticker := time.NewTicker(time.Minute) defer ticker.Stop() for range ticker.C { - if wa.active { - go makeActivityRequest() - wa.active = false + if tr.active { + go tr.makeActivityRequest() + tr.active = false } } } -func makeActivityRequest() { - req, err := http.NewRequest(http.MethodPut, APIEndpoint+"/activity/"+workspaceID, nil) +func (tr *tracker) makeActivityRequest() { + req, err := http.NewRequest(http.MethodPut, tr.activityAPI, nil) if err != nil { panic(err) } diff --git a/agents/go-agents/src/main/go/core/activity/noop.go b/agents/go-agents/src/main/go/core/activity/noop.go new file mode 100644 index 00000000000..9a044a74a22 --- /dev/null +++ b/agents/go-agents/src/main/go/core/activity/noop.go @@ -0,0 +1,23 @@ +// +// 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 activity + +// NoOpActivityTracker does nothing +type NoOpActivityTracker struct { + WorkspaceActivityTracker +} + +// Notify does nothing +func (tr *NoOpActivityTracker) Notify() {} + +// StartTracking does nothing +func (tr *NoOpActivityTracker) StartTracking() {} diff --git a/agents/go-agents/src/main/go/core/common/error.go b/agents/go-agents/src/main/go/core/common/error.go new file mode 100644 index 00000000000..7b36795e557 --- /dev/null +++ b/agents/go-agents/src/main/go/core/common/error.go @@ -0,0 +1,22 @@ +// +// 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 common + +import "log" + +// LogError logs error if it is not nil. +// Useful in situations when error is not processed in any case but its logging is needed. +func LogError(err error) { + if err != nil { + log.Println(err) + } +} diff --git a/agents/go-agents/src/main/go/core/process/file_logger.go b/agents/go-agents/src/main/go/core/process/file_logger.go index 099906a3db1..8842dad993c 100644 --- a/agents/go-agents/src/main/go/core/process/file_logger.go +++ b/agents/go-agents/src/main/go/core/process/file_logger.go @@ -43,7 +43,7 @@ func NewLogger(filename string) (*FileLogger, error) { if err != nil { return nil, err } - defer file.Close() + defer closeFile(file) return fl, nil } @@ -87,10 +87,16 @@ func (fl *FileLogger) doFlush() { if err != nil { log.Printf("Couldn't open file '%s' for flushing the buffer. %s \n", fl.filename, err.Error()) } else { - defer f.Close() + defer closeFile(f) _, err = fl.buffer.WriteTo(f) if err != nil { log.Printf("Error appears on flushing data to file '%s'. %s \n", fl.filename, err.Error()) } } } + +func closeFile(file *os.File) { + if err := file.Close(); err != nil { + log.Printf("Can't close file %s. Error: %s", file.Name(), err) + } +} diff --git a/agents/go-agents/src/main/go/core/process/file_logger_test.go b/agents/go-agents/src/main/go/core/process/file_logger_test.go index c836f4354e3..077e4ad66d2 100644 --- a/agents/go-agents/src/main/go/core/process/file_logger_test.go +++ b/agents/go-agents/src/main/go/core/process/file_logger_test.go @@ -14,6 +14,7 @@ package process_test import ( "encoding/json" "io/ioutil" + "log" "math/rand" "os" "testing" @@ -26,7 +27,7 @@ var alphabet = []byte("abcdefgh123456789") func TestFileLoggerCreatesFileWhenFileDoesNotExist(t *testing.T) { filename := os.TempDir() + string(os.PathSeparator) + randomName(10) - defer os.Remove(filename) + defer removeFile(filename) if _, err := os.Stat(filename); err == nil { t.Fatalf("File '%s' already exists", filename) @@ -43,7 +44,7 @@ func TestFileLoggerCreatesFileWhenFileDoesNotExist(t *testing.T) { func TestFileLoggerTruncatesFileIfFileExistsOnCreate(t *testing.T) { filename := os.TempDir() + string(os.PathSeparator) + randomName(10) - defer os.Remove(filename) + defer removeFile(filename) if _, err := os.Create(filename); err != nil { t.Fatal(err) @@ -67,7 +68,7 @@ func TestFileLoggerTruncatesFileIfFileExistsOnCreate(t *testing.T) { func TestLogsAreFlushedOnClose(t *testing.T) { filename := os.TempDir() + string(os.PathSeparator) + randomName(10) - defer os.Remove(filename) + defer removeFile(filename) fl, err := process.NewLogger(filename) if err != nil { @@ -120,3 +121,9 @@ func randomName(length int) string { } return string(bytes) } + +func removeFile(path string) { + if err := os.Remove(path); err != nil { + log.Printf("Can't remove file %s. Error: %s", path, err) + } +} diff --git a/agents/go-agents/src/main/go/core/process/logs_reader.go b/agents/go-agents/src/main/go/core/process/logs_reader.go index 3c08ffd6ce0..5712ad70db2 100644 --- a/agents/go-agents/src/main/go/core/process/logs_reader.go +++ b/agents/go-agents/src/main/go/core/process/logs_reader.go @@ -54,7 +54,7 @@ func (lr *LogsReader) ReadLogs() ([]*LogMessage, error) { if err != nil { return nil, err } - defer logsFile.Close() + defer closeFile(logsFile) from := time.Time{} if lr.readFrom != nil { diff --git a/agents/go-agents/src/main/go/core/process/logs_reader_test.go b/agents/go-agents/src/main/go/core/process/logs_reader_test.go index b30413bff54..5fac7e5de42 100644 --- a/agents/go-agents/src/main/go/core/process/logs_reader_test.go +++ b/agents/go-agents/src/main/go/core/process/logs_reader_test.go @@ -21,7 +21,7 @@ import ( func TestReadLogs(t *testing.T) { filename := os.TempDir() + string(os.PathSeparator) + randomName(10) - defer os.Remove(filename) + defer removeFile(filename) fl, err := process.NewLogger(filename) if err != nil { diff --git a/agents/go-agents/src/main/go/core/rest/default_router.go b/agents/go-agents/src/main/go/core/rest/default_router.go new file mode 100644 index 00000000000..08abd0effe0 --- /dev/null +++ b/agents/go-agents/src/main/go/core/rest/default_router.go @@ -0,0 +1,100 @@ +// +// 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 rest + +import ( + "net/http" + "regexp" + + "fmt" + + "github.com/julienschmidt/httprouter" +) + +// DefaultRouter is router backed by github.com/julienschmidt/httprouter http router +type DefaultRouter struct { + router *httprouter.Router +} + +type basePathChoppingRouter struct { + DefaultRouter + reg *regexp.Regexp +} + +// NewDefaultRouter creates new instance of default implementation of router. +// basePath is path that should be cut from origin HTTP path and then matched to restRoutes. +func NewDefaultRouter(basePath string, restRoutes []RoutesGroup) Router { + var jrouter = httprouter.New() + + // base path chopping is not needed + if basePath == "" { + defaultRouter := &DefaultRouter{ + router: jrouter, + } + defaultRouter.addRoutes(restRoutes) + return defaultRouter + } + + // base path chopping is needed + reg, err := regexp.Compile(basePath) + if err != nil { + panic(fmt.Errorf("Base path '%s' is not a regexp. Error: %s", basePath, err)) + } + defaultRouter := &basePathChoppingRouter{ + DefaultRouter: DefaultRouter{router: jrouter}, + reg: reg, + } + defaultRouter.addRoutes(restRoutes) + return defaultRouter +} + +func (router *DefaultRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + router.router.ServeHTTP(w, r) +} + +func (router *DefaultRouter) addRoutes(restRoutes []RoutesGroup) { + for _, routesGroup := range restRoutes { + for _, route := range routesGroup.Items { + router.router.Handle( + route.Method, + route.Path, + toHandle(route.HandleFunc), + ) + } + } +} + +func (router *basePathChoppingRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // if reg is prefix of request path remove the prefix + if idx := router.reg.FindStringSubmatchIndex(r.URL.Path); len(idx) != 0 && idx[0] == 0 { + r.URL.Path = r.URL.Path[idx[1]:] + r.RequestURI = r.RequestURI[idx[1]:] + } + router.router.ServeHTTP(w, r) +} + +func toHandle(f HTTPRouteHandlerFunc) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + if err := f(w, r, routerParamsAdapter{params: p}); err != nil { + WriteError(w, err) + } + } +} + +// Implementation of route.Params +type routerParamsAdapter struct { + params httprouter.Params +} + +func (pa routerParamsAdapter) Get(param string) string { + return pa.params.ByName(param) +} diff --git a/agents/go-agents/src/main/go/core/rest/errors.go b/agents/go-agents/src/main/go/core/rest/errors.go index 32c8fe82d33..584eaab5882 100644 --- a/agents/go-agents/src/main/go/core/rest/errors.go +++ b/agents/go-agents/src/main/go/core/rest/errors.go @@ -15,27 +15,33 @@ import ( "net/http" ) -type ApiError struct { +// APIError represents http error +type APIError struct { error Code int } +// BadRequest represents http error with 400 code func BadRequest(err error) error { - return ApiError{err, http.StatusBadRequest} + return APIError{err, http.StatusBadRequest} } +// NotFound represents http error with code 404 func NotFound(err error) error { - return ApiError{err, http.StatusNotFound} + return APIError{err, http.StatusNotFound} } +// Conflict represents http error with 409 code func Conflict(err error) error { - return ApiError{err, http.StatusConflict} + return APIError{err, http.StatusConflict} } +// Forbidden represents http error with 403 code func Forbidden(err error) error { - return ApiError{err, http.StatusForbidden} + return APIError{err, http.StatusForbidden} } +// Unauthorized represents http error with 401 code func Unauthorized(err error) error { - return ApiError{err, http.StatusUnauthorized} + return APIError{err, http.StatusUnauthorized} } diff --git a/agents/go-agents/src/main/go/core/rest/route.go b/agents/go-agents/src/main/go/core/rest/route.go index 61aca1bae5d..ffbc37a47ce 100644 --- a/agents/go-agents/src/main/go/core/rest/route.go +++ b/agents/go-agents/src/main/go/core/rest/route.go @@ -73,7 +73,7 @@ func (r *Route) String() string { // WriteError writes error into response func WriteError(w http.ResponseWriter, err error) { - if apiErr, ok := err.(ApiError); ok { + if apiErr, ok := err.(APIError); ok { http.Error(w, apiErr.Error(), apiErr.Code) } else { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/agents/go-agents/src/main/go/core/rest/router.go b/agents/go-agents/src/main/go/core/rest/router.go new file mode 100644 index 00000000000..777f95e7e6d --- /dev/null +++ b/agents/go-agents/src/main/go/core/rest/router.go @@ -0,0 +1,34 @@ +// +// 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 rest + +import ( + "log" + "net/http" +) + +// Router provides http requests routing capabilities +type Router interface { + http.Handler +} + +// PrintRoutes prints description of routes in provided slice of groups +func PrintRoutes(restRoutes []RoutesGroup) { + log.Print("⇩ Registered HTTPRoutes:\n\n") + for _, routesGroup := range restRoutes { + log.Printf("%s:\n", routesGroup.Name) + for _, route := range routesGroup.Items { + log.Printf("✓ %s\n", &route) + } + log.Println() + } +} diff --git a/agents/go-agents/src/main/go/core/rpc/channels.go b/agents/go-agents/src/main/go/core/rpc/channels.go index 8bef409e838..7b4f9654cbf 100644 --- a/agents/go-agents/src/main/go/core/rpc/channels.go +++ b/agents/go-agents/src/main/go/core/rpc/channels.go @@ -45,6 +45,9 @@ var ( channels = channelsMap{items: make(map[string]Channel)} + // PingPeriod defines period of WS pings + PingPeriod = 60 * time.Second + // HTTPRoutes for this package that should be registered HTTPRoutes = rest.RoutesGroup{ Name: "Channel Routes", @@ -187,6 +190,8 @@ func registerChannel(w http.ResponseWriter, r *http.Request, _ rest.Params) erro log.Printf("A new channel with id '%s' successfully opened", channel.ID) + // send ping messages + go setupWSPinging(conn) go transferAsJSON(conn, channel.output) go redirectEventsToOutput(channel) go handleMessages(readMessages(conn), channel) @@ -271,6 +276,18 @@ func transferAsJSON(conn *websocket.Conn, c chan interface{}) { } } +func setupWSPinging(conn *websocket.Conn) { + ticker := time.NewTicker(PingPeriod) + defer ticker.Stop() + // send ping messages by sheduler + for range ticker.C { + if err := conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + log.Printf("Error occurs on sending ping message to websocket. %v", err) + return + } + } +} + // handles messages as jsonrpc as described by package doc type jsonrpc2_0MessageHandler struct{} diff --git a/agents/go-agents/src/main/go/core/rpc/route.go b/agents/go-agents/src/main/go/core/rpc/route.go index 4c8ece61d16..9130039833b 100644 --- a/agents/go-agents/src/main/go/core/rpc/route.go +++ b/agents/go-agents/src/main/go/core/rpc/route.go @@ -69,20 +69,36 @@ func (routes *routesMap) get(method string) (Route, bool) { // Returns true if route is added and false if route for such method // already present(won't override it). -func (or *routesMap) add(r Route) bool { +func (routes *routesMap) add(route Route) bool { routes.Lock() defer routes.Unlock() - _, ok := routes.items[r.Method] + _, ok := routes.items[route.Method] if ok { return false } - routes.items[r.Method] = r + routes.items[route.Method] = route return true } -// RegisterRoute adds a new route, panics if such route already exists. -func RegisterRoute(route Route) { - if !routes.add(route) { - log.Fatalf("Couldn't register a new route, route for the operation '%s' already exists", route.Method) +// RegisterRoutes adds provided routes groups into routes. +// Panics if any of routes already exists or duplicated in a parameter. +func RegisterRoutes(rg []RoutesGroup) { + for _, group := range rg { + for _, route := range group.Items { + if !routes.add(route) { + log.Panicf("Couldn't register a new route, route for the operation '%s' already exists", route.Method) + } + } + } +} + +// PrintRoutes prints provided rpc routes by group +func PrintRoutes(rg []RoutesGroup) { + log.Print("⇩ Registered RPCRoutes:\n\n") + for _, group := range rg { + log.Printf("%s:\n", group.Name) + for _, route := range group.Items { + log.Printf("✓ %s\n", route.Method) + } } } diff --git a/agents/go-agents/src/main/go/exec-agent/exec/logs_distributor_test.go b/agents/go-agents/src/main/go/exec-agent/exec/logs_distributor_test.go index 5d56b1e2645..328de495888 100644 --- a/agents/go-agents/src/main/go/exec-agent/exec/logs_distributor_test.go +++ b/agents/go-agents/src/main/go/exec-agent/exec/logs_distributor_test.go @@ -14,6 +14,7 @@ package exec_test import ( "fmt" "io/ioutil" + "log" "os" "testing" @@ -22,7 +23,7 @@ import ( func TestLogsDistributorCreatesSubdirectories(t *testing.T) { baseDir := os.TempDir() + string(os.PathSeparator) + randomName(10) - defer os.RemoveAll(baseDir) + defer removeAll(baseDir) distributor := exec.DefaultLogsDistributor{ MaxDirsCount: 4, @@ -42,7 +43,7 @@ func TestLogsDistributorCreatesSubdirectories(t *testing.T) { func TestLogsDistribution(t *testing.T) { baseDir := os.TempDir() + string(os.PathSeparator) + randomName(10) - defer os.RemoveAll(baseDir) + defer removeAll(baseDir) distributor := exec.DefaultLogsDistributor{ MaxDirsCount: 4, @@ -71,3 +72,9 @@ func TestLogsDistribution(t *testing.T) { } } } + +func removeAll(path string) { + if err := os.RemoveAll(path); err != nil { + log.Printf("Can't remove folder %s. Error: %s", path, err) + } +} diff --git a/agents/go-agents/src/main/go/exec-agent/exec/process.go b/agents/go-agents/src/main/go/exec-agent/exec/process.go index 1e866b1568d..e21f26d760b 100644 --- a/agents/go-agents/src/main/go/exec-agent/exec/process.go +++ b/agents/go-agents/src/main/go/exec-agent/exec/process.go @@ -14,6 +14,7 @@ package exec import ( "errors" "fmt" + "log" "os" "os/exec" "sync" @@ -26,29 +27,39 @@ import ( ) const ( - StdoutBit = 1 << iota - StderrBit = 1 << iota + // StdoutBit is set when subscriber is interested in stdout logs + StdoutBit = 1 << iota + // StderrBit is set when subscriber is interested in stdout logs + StderrBit = 1 << iota + // ProcessStatusBit is set when subscriber is interested in a process events ProcessStatusBit = 1 << iota - DefaultMask = StderrBit | StdoutBit | ProcessStatusBit + // DefaultMask is set by default and identifies receiving of both logs and events of a process + DefaultMask = StderrBit | StdoutBit | ProcessStatusBit + // DefaultShellInterpreter is default shell that executes commands + // unless another one is configured DefaultShellInterpreter = "/bin/bash" ) var ( - prevPid uint64 = 0 - processes = &processesMap{items: make(map[uint64]*MachineProcess)} - logsDist = NewLogsDistributor() - LogsDir string - ShellInterpreter string = DefaultShellInterpreter + prevPid uint64 + // LogsDir directory where logs of processes should be stored + LogsDir string + + processes = &processesMap{items: make(map[uint64]*MachineProcess)} + logsDist = NewLogsDistributor() + // ShellInterpreter is shell that executes commands + ShellInterpreter = DefaultShellInterpreter ) +// Command represents command that is used in command execution API type Command struct { Name string `json:"name"` CommandLine string `json:"commandLine"` Type string `json:"type"` } -// Defines machine process model +// MachineProcess defines machine process model type MachineProcess struct { // The virtual id of the process, it is guaranteed that pid @@ -107,17 +118,20 @@ type MachineProcess struct { beforeEventsHook func(process MachineProcess) } +// Subscriber receives process logs type Subscriber struct { ID string Mask uint64 Channel chan *rpc.Event } +// NoProcessError is returned when requested process doesn't exist type NoProcessError struct { error Pid uint64 } +// NotAliveError is returned when process that is target of an action is not alive anymore type NotAliveError struct { error Pid uint64 @@ -129,6 +143,7 @@ type processesMap struct { items map[uint64]*MachineProcess } +// Start starts MachineProcess func Start(process MachineProcess) (MachineProcess, error) { // wrap command to be able to kill child processes see https://github.com/golang/go/issues/8854 cmd := exec.Command("setsid", ShellInterpreter, "-c", process.CommandLine) @@ -205,7 +220,7 @@ func Start(process MachineProcess) (MachineProcess, error) { return process, nil } -// Gets process by pid. +// Get retrieves process by pid. // If process doesn't exist then error of type NoProcessError is returned. func Get(pid uint64) (MachineProcess, error) { p, ok := directGet(pid) @@ -215,6 +230,9 @@ func Get(pid uint64) (MachineProcess, error) { return MachineProcess{}, noProcess(pid) } +// GetProcesses retrieves list of processes. +// If parameter all is true then returns all processes, +// otherwice returns only live processes func GetProcesses(all bool) []MachineProcess { processes.RLock() defer processes.RUnlock() @@ -234,7 +252,7 @@ func GetProcesses(all bool) []MachineProcess { return pArr } -// Kills process by given pid. +// Kill kills process by given pid. // Returns an error when any error occurs during process kill. // If process doesn't exist error of type NoProcessError is returned. func Kill(pid uint64) error { @@ -249,7 +267,7 @@ func Kill(pid uint64) error { return syscall.Kill(-p.NativePid, syscall.SIGKILL) } -// Reads process logs between [from, till] inclusive. +// ReadLogs reads process logs between [from, till] inclusive. // Returns an error if any error occurs during logs reading. // If process doesn't exist error of type NoProcessError is returned. func ReadLogs(pid uint64, from time.Time, till time.Time) ([]*proc.LogMessage, error) { @@ -264,14 +282,14 @@ func ReadLogs(pid uint64, from time.Time, till time.Time) ([]*proc.LogMessage, e return proc.NewLogsReader(p.logfileName).From(from).Till(till).ReadLogs() } -// Reads all process logs. +// ReadAllLogs reads all process logs. // Returns an error if any error occurs during logs reading. // If process doesn't exist error of type NoProcessError is returned. func ReadAllLogs(pid uint64) ([]*proc.LogMessage, error) { return ReadLogs(pid, time.Time{}, time.Now()) } -// Unsubscribe subscriber with given id from process events. +// RemoveSubscriber unsubscribes subscriber with given id from process events. // If process doesn't exist then error of type NoProcessError is returned. func RemoveSubscriber(pid uint64, id string) error { p, ok := directGet(pid) @@ -292,7 +310,7 @@ func RemoveSubscriber(pid uint64, id string) error { return nil } -// Subscribe to the process output. +// AddSubscriber subscribes provided subscriber to the process output. // An error of type NoProcessError is returned when process // with given pid doesn't exist, a regular error is returned // if the process is dead or subscriber with such id already subscribed @@ -316,7 +334,7 @@ func AddSubscriber(pid uint64, subscriber Subscriber) error { return nil } -// Adds a new process subscriber by reading all the logs between +// RestoreSubscriber adds a new process subscriber by reading all the logs between // given 'after' and now and publishing them to the channel. // Returns an error of type NoProcessError if process with given id doesn't exist, // returns a regular error if process is alive an subscriber with such id @@ -368,7 +386,7 @@ func RestoreSubscriber(pid uint64, subscriber Subscriber, after time.Time) error return nil } -// Updates subscriber with given id. +// UpdateSubscriber updates subscriber with given id. // An error of type NoProcessError is returned when process // with given pid doesn't exist, a regular error is returned // if the process is dead. @@ -388,54 +406,61 @@ func UpdateSubscriber(pid uint64, id string, newMask uint64) error { return nil } } - return errors.New(fmt.Sprintf("No subscriber with id '%s'", id)) + return fmt.Errorf("No subscriber with id '%s'", id) } +// OnStdout notifies subscribers about new output in stdout func (process *MachineProcess) OnStdout(line string, time time.Time) { process.notifySubs(newStdoutEvent(process.Pid, line, time), StdoutBit) } +// OnStderr notifies subscribers about new output in stderr func (process *MachineProcess) OnStderr(line string, time time.Time) { process.notifySubs(newStderrEvent(process.Pid, line, time), StderrBit) } -func (mp *MachineProcess) Close() { +// Close cleanups process resources and notifies subscribers about process death +func (process *MachineProcess) Close() { // Cleanup command resources - mp.command.Wait() + if err := process.command.Wait(); err != nil { + if _, ok := err.(*exec.ExitError); !ok { + log.Printf("Error occurs on process cleanup. %s", err) + } + } // Cleanup machine process resources before dead event is sent - mp.mutex.Lock() - mp.Alive = false - mp.command = nil - mp.pumper = nil - mp.mutex.Unlock() + process.mutex.Lock() + process.Alive = false + process.command = nil + process.pumper = nil + process.mutex.Unlock() - mp.notifySubs(newDiedEvent(*mp), ProcessStatusBit) + process.notifySubs(newDiedEvent(*process), ProcessStatusBit) - mp.mutex.Lock() - mp.subs = nil - mp.mutex.Unlock() + process.mutex.Lock() + process.subs = nil + process.mutex.Unlock() - mp.updateLastUsedTime() + process.updateLastUsedTime() } -func (p *MachineProcess) notifySubs(event *rpc.Event, typeBit uint64) { - p.mutex.RLock() - subs := p.subs +func (process *MachineProcess) notifySubs(event *rpc.Event, typeBit uint64) { + process.mutex.RLock() + subs := process.subs for _, subscriber := range subs { // Check whether subscriber needs such kind of event and then try to notify it if subscriber.Mask&typeBit == typeBit && !tryWrite(subscriber.Channel, event) { // Impossible to write to the channel, remove the channel from the subscribers list. // It may happen when writing to the closed channel - defer RemoveSubscriber(p.Pid, subscriber.ID) + defer RemoveSubscriber(process.Pid, subscriber.ID) } } - p.mutex.RUnlock() + process.mutex.RUnlock() } -func (mp *MachineProcess) updateLastUsedTime() { - mp.lastUsedLock.Lock() - mp.lastUsed = time.Now() - mp.lastUsedLock.Unlock() +func (process *MachineProcess) updateLastUsedTime() { + process.lastUsedLock.Lock() + process.lastUsed = time.Now() + process.lastUsedLock.Unlock() } // Writes to a channel and returns true if write is successful, @@ -463,7 +488,7 @@ func directGet(pid uint64) (*MachineProcess, bool) { // Returns an error indicating that process with given pid doesn't exist func noProcess(pid uint64) *NoProcessError { return &NoProcessError{ - error: errors.New(fmt.Sprintf("Process with id '%d' does not exist", pid)), + error: fmt.Errorf("Process with id '%d' does not exist", pid), Pid: pid, } } @@ -471,7 +496,7 @@ func noProcess(pid uint64) *NoProcessError { // Returns an error indicating that process with given pid is not alive func notAlive(pid uint64) *NotAliveError { return &NotAliveError{ - error: errors.New(fmt.Sprintf("Process with id '%d' is not alive", pid)), + error: fmt.Errorf("Process with id '%d' is not alive", pid), Pid: pid, } } diff --git a/agents/go-agents/src/main/go/exec-agent/exec/process_cleaner_test.go b/agents/go-agents/src/main/go/exec-agent/exec/process_cleaner_test.go index fca2f524ff5..f9444205a8a 100644 --- a/agents/go-agents/src/main/go/exec-agent/exec/process_cleaner_test.go +++ b/agents/go-agents/src/main/go/exec-agent/exec/process_cleaner_test.go @@ -40,7 +40,7 @@ func TestCleansOnlyUnusedProcesses(t *testing.T) { time.Sleep(500 * time.Millisecond) // use one of the processes, so it is used now - exec.Get(p1.Pid) + _, _ = exec.Get(p1.Pid) // cleanup immediately (&exec.Cleaner{CleanupPeriod: 0, CleanupThreshold: 500 * time.Millisecond}).CleanOnce() diff --git a/agents/go-agents/src/main/go/exec-agent/exec/process_test.go b/agents/go-agents/src/main/go/exec-agent/exec/process_test.go index e614fd0f97d..74a7efb1d45 100644 --- a/agents/go-agents/src/main/go/exec-agent/exec/process_test.go +++ b/agents/go-agents/src/main/go/exec-agent/exec/process_test.go @@ -12,6 +12,7 @@ package exec_test import ( + "log" "math/rand" "os" "strings" @@ -143,10 +144,10 @@ func TestRestoreSubscriberForDeadProcess(t *testing.T) { done <- true }() - exec.RestoreSubscriber(p.Pid, exec.Subscriber{ - "test", - exec.DefaultMask, - channel, + _ = exec.RestoreSubscriber(p.Pid, exec.Subscriber{ + ID: "test", + Mask: exec.DefaultMask, + Channel: channel, }, beforeStart) <-done @@ -201,7 +202,7 @@ func TestReadProcessLogs(t *testing.T) { } } -func startAndWaitTestProcess(cmd string, t *testing.T) exec.MachineProcess { +func startAndWaitTestProcess(cmd string, t *testing.T) *exec.MachineProcess { exec.LogsDir = TmpFile() events := make(chan *rpc.Event) done := make(chan bool) @@ -248,7 +249,7 @@ func startAndWaitTestProcess(cmd string, t *testing.T) exec.MachineProcess { if err != nil { t.Fatal(err) } - return result + return &result } func TmpFile() string { @@ -256,7 +257,9 @@ func TmpFile() string { } func cleanupLogsDir() { - os.RemoveAll(exec.LogsDir) + if err := os.RemoveAll(exec.LogsDir); err != nil { + log.Printf("Can't remove folder %s. Error: %s", exec.LogsDir, err) + } } func randomName(length int) string { diff --git a/agents/go-agents/src/main/go/exec-agent/exec/rest_service.go b/agents/go-agents/src/main/go/exec-agent/exec/rest_service.go index a95152b3952..e38425b535e 100644 --- a/agents/go-agents/src/main/go/exec-agent/exec/rest_service.go +++ b/agents/go-agents/src/main/go/exec-agent/exec/rest_service.go @@ -15,6 +15,7 @@ import ( "errors" "fmt" "io" + "log" "math" "net/http" "strconv" @@ -127,29 +128,63 @@ func killProcessHF(w http.ResponseWriter, r *http.Request, p rest.Params) error return nil } +type getLogsParams struct { + pid uint64 + from time.Time + till time.Time + limit int + skip int + format string +} + func getProcessLogsHF(w http.ResponseWriter, r *http.Request, p rest.Params) error { + logsParams, err := parseGetLogsParameters(r, p) + if err != nil { + return err + } + + logs, err := ReadLogs(logsParams.pid, logsParams.from, logsParams.till) + if err != nil { + return asHTTPError(err) + } + + len := len(logs) + fromIdx := int(math.Max(float64(len-logsParams.limit-logsParams.skip), 0)) + toIdx := len - int(math.Min(float64(logsParams.skip), float64(len))) + + // Respond with an appropriate logs format, default json + switch strings.ToLower(logsParams.format) { + case "text": + for _, item := range logs[fromIdx:toIdx] { + line := fmt.Sprintf("[%s] %s \t %s\n", item.Kind, item.Time.Format(process.DateTimeFormat), item.Text) + if _, err := io.WriteString(w, line); err != nil { + log.Printf("Error occurs on writing logs of process %v into response. %s", logsParams.pid, err) + } + } + default: + return restutil.WriteJSON(w, logs[fromIdx:toIdx]) + } + return nil +} + +func parseGetLogsParameters(r *http.Request, p rest.Params) (*getLogsParams, error) { pid, err := parsePid(p.Get("pid")) if err != nil { - return rest.BadRequest(err) + return nil, rest.BadRequest(err) } // Parse 'from', if 'from' is not specified then read all the logs from the start // if 'from' format is different from the DATE_TIME_FORMAT then return 400 from, err := process.ParseTime(r.URL.Query().Get("from"), time.Time{}) if err != nil { - return rest.BadRequest(errors.New("Bad format of 'from', " + err.Error())) + return nil, rest.BadRequest(errors.New("Bad format of 'from', " + err.Error())) } // Parse 'till', if 'till' is not specified then 'now' is used for it // if 'till' format is different from the DATE_TIME_FORMAT then return 400 till, err := process.ParseTime(r.URL.Query().Get("till"), time.Now()) if err != nil { - return rest.BadRequest(errors.New("Bad format of 'till', " + err.Error())) - } - - logs, err := ReadLogs(pid, from, till) - if err != nil { - return asHTTPError(err) + return nil, rest.BadRequest(errors.New("Bad format of 'till', " + err.Error())) } // limit logs from the latest to the earliest @@ -158,27 +193,22 @@ func getProcessLogsHF(w http.ResponseWriter, r *http.Request, p rest.Params) err limit := restutil.IntQueryParam(r, "limit", DefaultLogsPerPageLimit) skip := restutil.IntQueryParam(r, "skip", 0) if limit < 1 { - return rest.BadRequest(errors.New("Required 'limit' to be > 0")) + return nil, rest.BadRequest(errors.New("Required 'limit' to be > 0")) } if skip < 0 { - return rest.BadRequest(errors.New("Required 'skip' to be >= 0")) + return nil, rest.BadRequest(errors.New("Required 'skip' to be >= 0")) } - len := len(logs) - fromIdx := int(math.Max(float64(len-limit-skip), 0)) - toIdx := len - int(math.Min(float64(skip), float64(len))) - // Respond with an appropriate logs format, default json format := r.URL.Query().Get("format") - switch strings.ToLower(format) { - case "text": - for _, item := range logs[fromIdx:toIdx] { - line := fmt.Sprintf("[%s] %s \t %s\n", item.Kind, item.Time.Format(process.DateTimeFormat), item.Text) - io.WriteString(w, line) - } - default: - return restutil.WriteJSON(w, logs[fromIdx:toIdx]) - } - return nil + + return &getLogsParams{ + pid: pid, + from: from, + till: till, + limit: limit, + skip: skip, + format: format, + }, nil } func getProcessesHF(w http.ResponseWriter, r *http.Request, _ rest.Params) error { diff --git a/agents/go-agents/src/main/go/exec-agent/main.go b/agents/go-agents/src/main/go/exec-agent/main.go index a99f0c2bdfd..b85ff4daaba 100644 --- a/agents/go-agents/src/main/go/exec-agent/main.go +++ b/agents/go-agents/src/main/go/exec-agent/main.go @@ -13,37 +13,20 @@ package main import ( "flag" - "fmt" "log" "net/http" "net/url" "os" "time" - "regexp" - - "github.com/eclipse/che/agents/go-agents/src/main/go/core/activity" "github.com/eclipse/che/agents/go-agents/src/main/go/core/auth" "github.com/eclipse/che/agents/go-agents/src/main/go/core/rest" "github.com/eclipse/che/agents/go-agents/src/main/go/core/rpc" "github.com/eclipse/che/agents/go-agents/src/main/go/exec-agent/exec" - "github.com/eclipse/che/agents/go-agents/src/main/go/exec-agent/term" - "github.com/julienschmidt/httprouter" ) var ( - AppHTTPRoutes = []rest.RoutesGroup{ - exec.HTTPRoutes, - rpc.HTTPRoutes, - term.HTTPRoutes, - } - - AppOpRoutes = []rpc.RoutesGroup{ - exec.RPCRoutes, - } - serverAddress string - staticDir string basePath string apiEndpoint string @@ -62,12 +45,6 @@ func init() { ":9000", "IP:PORT or :PORT the address to start the server on", ) - flag.StringVar( - &staticDir, - "static", - "./static/", - "path to the directory where static content is located", - ) flag.StringVar( &basePath, "path", @@ -80,20 +57,12 @@ func init() { Regexp syntax is supported`, ) - // terminal configuration - flag.StringVar( - &term.Cmd, - "cmd", - "/bin/bash", - "shell interpreter and command to execute on slave side of the pty", - ) - // workspace master server configuration flag.StringVar( &apiEndpoint, "api-endpoint", os.Getenv("CHE_API"), - `api-endpoint used by exec-agent modules(such as activity checker or authentication) + `api-endpoint used by exec-agent modules(such as authentication) to request workspace master. By default the value from 'CHE_API' environment variable is used`, ) @@ -111,15 +80,13 @@ func init() { "how much time machine tokens stay in cache(if auth is enabled)", ) - // terminal configuration - flag.BoolVar( - &activity.ActivityTrackingEnabled, - "enable-activity-tracking", - false, - "whether workspace master will be notified about workspace activity", - ) - // process executor configuration + flag.StringVar( + &exec.ShellInterpreter, + "cmd", + "/bin/bash", + "shell interpreter", + ) flag.IntVar( &processCleanupPeriodInMinutes, "process-cleanup-period", @@ -133,7 +100,10 @@ func init() { if -1 passed then processes won't be cleaned at all. Please note that the time of real cleanup is between configured threshold and threshold + process-cleanup-period.`, ) - curDir, _ := os.Getwd() + curDir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } curDir += string(os.PathSeparator) + "logs" flag.StringVar( &exec.LogsDir, @@ -148,101 +118,38 @@ func main() { log.SetOutput(os.Stdout) - // print configuration - fmt.Println("Exec-agent configuration") - fmt.Println(" Server") - fmt.Printf(" - Address: %s\n", serverAddress) - fmt.Printf(" - Static content: %s\n", staticDir) - fmt.Printf(" - Base path: '%s'\n", basePath) - fmt.Println(" Terminal") - fmt.Printf(" - Slave command: '%s'\n", term.Cmd) - fmt.Printf(" - Activity tracking enabled: %t\n", activity.ActivityTrackingEnabled) - if authEnabled { - fmt.Println(" Authentication") - fmt.Printf(" - Enabled: %t\n", authEnabled) - fmt.Printf(" - Tokens expiration timeout: %dm\n", tokensExpirationTimeoutInMinutes) - } - fmt.Println(" Process executor") - fmt.Printf(" - Logs dir: %s\n", exec.LogsDir) - if processCleanupPeriodInMinutes > 0 { - fmt.Printf(" - Cleanup job period: %dm\n", processCleanupPeriodInMinutes) - fmt.Printf(" - Not used & dead processes stay for: %dm\n", processCleanupThresholdInMinutes) - } - if authEnabled || activity.ActivityTrackingEnabled { - fmt.Println(" Workspace master server") - fmt.Printf(" - API endpoint: %s\n", apiEndpoint) - } - fmt.Println() - - activity.APIEndpoint = apiEndpoint + printConfiguration() - // process configuration + // remove old logs if err := os.RemoveAll(exec.LogsDir); err != nil { log.Fatal(err) } + // start cleaner routine if processCleanupPeriodInMinutes > 0 { if processCleanupThresholdInMinutes < 0 { log.Fatal("Expected process cleanup threshold to be non negative value") } cleaner := exec.NewCleaner(processCleanupPeriodInMinutes, processCleanupThresholdInMinutes) - cleaner.CleanPeriodically() - } - exec.ShellInterpreter = term.Cmd - - // terminal configuration - if activity.ActivityTrackingEnabled { - go activity.Tracker.StartTracking() - } - - // register routes and http handlers - router := httprouter.New() - router.NotFound = http.FileServer(http.Dir(staticDir)) - - fmt.Print("⇩ Registered HTTPRoutes:\n\n") - for _, routesGroup := range AppHTTPRoutes { - fmt.Printf("%s:\n", routesGroup.Name) - for _, route := range routesGroup.Items { - router.Handle( - route.Method, - route.Path, - toHandle(route.HandleFunc), - ) - fmt.Printf("✓ %s\n", &route) - } - fmt.Println() + go cleaner.CleanPeriodically() } - fmt.Print("\n⇩ Registered RPCRoutes:\n\n") - for _, routesGroup := range AppOpRoutes { - fmt.Printf("%s:\n", routesGroup.Name) - for _, route := range routesGroup.Items { - fmt.Printf("✓ %s\n", route.Method) - rpc.RegisterRoute(route) - } + appHTTPRoutes := []rest.RoutesGroup{ + exec.HTTPRoutes, + rpc.HTTPRoutes, } - var handler http.Handler = router - - // required authentication for all the requests, if it is configured - if authEnabled { - cache := auth.NewCache(time.Minute*time.Duration(tokensExpirationTimeoutInMinutes), time.Minute*5) - - handler = auth.NewCachingHandler(handler, apiEndpoint, func(w http.ResponseWriter, req *http.Request, err error) { - dropChannelsWithExpiredToken(req.URL.Query().Get("token")) - http.Error(w, err.Error(), http.StatusUnauthorized) - }, cache) + appOpRoutes := []rpc.RoutesGroup{ + exec.RPCRoutes, } - // cut base path on requests, if it is configured - if basePath != "" { - if rx, err := regexp.Compile(basePath); err == nil { - handler = basePathChopper{rx, handler} - } else { - log.Fatal(err) - } - } + // register routes and http handlers + r := rest.NewDefaultRouter(basePath, appHTTPRoutes) + rest.PrintRoutes(appHTTPRoutes) + rpc.RegisterRoutes(appOpRoutes) + rpc.PrintRoutes(appOpRoutes) + var handler = getHandler(r) http.Handle("/", handler) server := &http.Server{ @@ -254,44 +161,48 @@ func main() { log.Fatal(server.ListenAndServe()) } -func dropChannelsWithExpiredToken(token string) { +func getHandler(h http.Handler) http.Handler { + // required authentication for all the requests, if it is configured + if authEnabled { + cache := auth.NewCache(time.Minute*time.Duration(tokensExpirationTimeoutInMinutes), time.Minute*5) + return auth.NewCachingHandler(h, apiEndpoint, droppingRPCChannelsUnauthorizedHandler, cache) + } + + return h +} + +func droppingRPCChannelsUnauthorizedHandler(w http.ResponseWriter, req *http.Request, err error) { + token := req.URL.Query().Get("token") for _, c := range rpc.GetChannels() { - u, err := url.ParseRequestURI(c.RequestURI) - if err != nil { + if u, err1 := url.ParseRequestURI(c.RequestURI); err1 != nil { log.Printf("Couldn't parse the RequestURI '%s' of channel '%s'", c.RequestURI, c.ID) } else if u.Query().Get("token") == token { log.Printf("Token for channel '%s' is expired, trying to drop the channel", c.ID) rpc.DropChannel(c.ID) } } + http.Error(w, err.Error(), http.StatusUnauthorized) } -type routerParamsAdapter struct { - params httprouter.Params -} - -func (pa routerParamsAdapter) Get(param string) string { - return pa.params.ByName(param) -} - -func toHandle(f rest.HTTPRouteHandlerFunc) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - if err := f(w, r, routerParamsAdapter{params: p}); err != nil { - rest.WriteError(w, err) - } +func printConfiguration() { + log.Println("Exec-agent configuration") + log.Println(" Server") + log.Printf(" - Address: %s\n", serverAddress) + log.Printf(" - Base path: '%s'\n", basePath) + if authEnabled { + log.Println(" Authentication") + log.Printf(" - Enabled: %t\n", authEnabled) + log.Printf(" - Tokens expiration timeout: %dm\n", tokensExpirationTimeoutInMinutes) } -} - -type basePathChopper struct { - pattern *regexp.Regexp - delegate http.Handler -} - -func (c basePathChopper) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // if request path starts with given base path - if idx := c.pattern.FindStringSubmatchIndex(r.URL.Path); len(idx) != 0 && idx[0] == 0 { - r.URL.Path = r.URL.Path[idx[1]:] - r.RequestURI = r.RequestURI[idx[1]:] + log.Println(" Process executor") + log.Printf(" - Logs dir: %s\n", exec.LogsDir) + if processCleanupPeriodInMinutes > 0 { + log.Printf(" - Cleanup job period: %dm\n", processCleanupPeriodInMinutes) + log.Printf(" - Not used & dead processes stay for: %dm\n", processCleanupThresholdInMinutes) + } + if authEnabled { + log.Println(" Workspace master server") + log.Printf(" - API endpoint: %s\n", apiEndpoint) } - c.delegate.ServeHTTP(w, r) + log.Println() } diff --git a/agents/go-agents/src/main/go/exec-agent/term/server.go b/agents/go-agents/src/main/go/exec-agent/term/server.go deleted file mode 100644 index b42e39f33fb..00000000000 --- a/agents/go-agents/src/main/go/exec-agent/term/server.go +++ /dev/null @@ -1,373 +0,0 @@ -// moved from https://github.com/eclipse/che-lib/tree/master/websocket-terminal - -package term - -/* - * websocket/pty proxy server: - * This program wires a websocket to a pty master. - * - * Usage: - * go build -o ws-pty-proxy server.go - * ./websocket-terminal -cmd /bin/bash -addr :9000 -static $HOME/src/websocket-terminal - * ./websocket-terminal -cmd /bin/bash -- -i - * - * TODO: - * * make more things configurable - * * switch back to binary encoding after fixing term.js (see index.html) - * * make errors return proper codes to the web client - * - * Copyright 2014 Al Tobey tobert@gmail.com - * MIT License, see the LICENSE file - */ - -import ( - "bufio" - "bytes" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "io/ioutil" - "log" - "net" - "net/http" - "os" - "os/exec" - "sync" - "syscall" - "unicode/utf8" - - "github.com/eclipse/che-lib/pty" - "github.com/eclipse/che-lib/websocket" - "github.com/eclipse/che/agents/go-agents/src/main/go/core/activity" - "github.com/eclipse/che/agents/go-agents/src/main/go/core/rest" -) - -type wsPty struct { - Cmd *exec.Cmd // pty builds on os.exec - PtyFile *os.File // a pty is simply an os.File -} - -//Stop terminal process and its child processes. In modern Unix systems terminal stops with help -// SIGHUP signal and we used such way too. SIGHUP signal used to send a signal to a process -// (or process group), it's signal meaning that pseudo or virtual terminal has been closed. -// Example: command is executed inside a terminal window and the terminal window is closed while -// the command process is still running. -// If the process receiving SIGHUP is a Unix shell, then as part of job control it will often intercept -// the signal and ensure that all stopped processes are continued before sending the signal to child -// processes (more precisely, process groups, represented internally be the shell as a "job"), which -// by default terminates them. -func (wp *wsPty) Close(finalizer *ReadWriteRoutingFinalizer) { - closeFile(wp.PtyFile, finalizer) - pid := wp.Cmd.Process.Pid - - if pgid, err := syscall.Getpgid(pid); err == nil { - if err := syscall.Kill(-pgid, syscall.SIGHUP); err != nil { - fmt.Printf("Failed to SIGHUP terminal process by pgid: '%d'. Cause: '%s'", pgid, err) - } - } - if err := syscall.Kill(pid, syscall.SIGHUP); err != nil { - fmt.Printf("Failed to SIGHUP terminal process by pid '%d'. Cause: '%s'", pid, err) - } -} - -// WebSocketMessage represents message sent over websocket connection -type WebSocketMessage struct { - Type string `json:"type"` - Data json.RawMessage `json:"data"` -} - -type ReadWriteRoutingFinalizer struct { - *sync.Mutex - readDone bool - writeDone bool - fileClosed bool -} - -var ( - upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - } - - Cmd string - - HTTPRoutes = rest.RoutesGroup{ - Name: "Terminal routes", - Items: []rest.Route{ - { - Method: "GET", - Name: "Connect to pty(webscoket)", - Path: "/pty", - HandleFunc: ConnectToPtyHF, - }, - }, - } -) - -func StartPty() (*wsPty, error) { - // TODO consider whether these args are needed at all - cmd := exec.Command(Cmd, flag.Args()...) - cmd.Env = append(os.Environ(), "TERM=xterm") - - file, err := pty.Start(cmd) - if err != nil { - return nil, err - } - - //Set the size of the pty - pty.Setsize(file, 60, 200) - - return &wsPty{ - PtyFile: file, - Cmd: cmd, - }, nil -} - -func isNormalWsError(err error) bool { - closeErr, ok := err.(*websocket.CloseError) - if ok && (closeErr.Code == websocket.CloseGoingAway || closeErr.Code == websocket.CloseNormalClosure) { - return true - } - _, ok = err.(*net.OpError) - return ok -} - -func isNormalPtyError(err error) bool { - if err == io.EOF { - return true - } - pathErr, ok := err.(*os.PathError) - return ok && - pathErr.Op == "read" && - pathErr.Path == "/dev/ptmx" && - pathErr.Err.Error() == "input/output error" -} - -// read from the web socket, copying to the pty master -// messages are expected to be text and base64 encoded -func sendConnectionInputToPty(conn *websocket.Conn, reader io.ReadCloser, wp *wsPty, finalizer *ReadWriteRoutingFinalizer) { - f := wp.PtyFile - defer closeReader(reader, f, finalizer) - - for { - mt, payload, err := conn.ReadMessage() - if err != nil { - if !isNormalWsError(err) { - log.Printf("conn.ReadMessage failed: %s\n", err) - } - return - } - switch mt { - case websocket.BinaryMessage: - log.Printf("Ignoring binary message: %q\n", payload) - case websocket.TextMessage: - var msg WebSocketMessage - if err := json.Unmarshal(payload, &msg); err != nil { - log.Printf("Invalid message %s\n", err) - continue - } - if msg.Type == "close" { - wp.Close(finalizer) - return - } - if errMsg := handleMessage(msg, f); errMsg != nil { - log.Print(errMsg.Error()) - return - } - - default: - log.Printf("Invalid websocket message type %d\n", mt) - return - } - } -} - -func handleMessage(msg WebSocketMessage, ptyFile *os.File) error { - switch msg.Type { - case "resize": - var size []float64 - if err := json.Unmarshal(msg.Data, &size); err != nil { - log.Printf("Invalid resize message: %s\n", err) - } else { - pty.Setsize(ptyFile, uint16(size[1]), uint16(size[0])) - activity.Tracker.Notify() - } - - case "data": - var dat string - if err := json.Unmarshal(msg.Data, &dat); err != nil { - log.Printf("Invalid data message %s\n", err) - } else { - ptyFile.Write([]byte(dat)) - activity.Tracker.Notify() - } - - default: - return errors.New("Invalid field message type: " + msg.Type + "\n") - } - return nil -} - -//read byte array as Unicode code points (rune in go) -func normalizeBuffer(normalizedBuf *bytes.Buffer, buf []byte, n int) (int, error) { - bufferBytes := normalizedBuf.Bytes() - runeReader := bufio.NewReader(bytes.NewReader(append(bufferBytes[:], buf[:n]...))) - normalizedBuf.Reset() - i := 0 - for i < n { - char, charLen, err := runeReader.ReadRune() - if err != nil { - return i, err - } - if char == utf8.RuneError { - runeReader.UnreadRune() - return i, nil - } - i += charLen - if _, err := normalizedBuf.WriteRune(char); err != nil { - return i, err - } - } - return i, nil -} - -// copy everything from the pty master to the websocket -// using base64 encoding for now due to limitations in term.js -func sendPtyOutputToConnection(conn *websocket.Conn, reader io.ReadCloser, finalizer *ReadWriteRoutingFinalizer) { - defer closeConn(conn, finalizer) - - buf := make([]byte, 8192) - var buffer bytes.Buffer - // TODO: more graceful exit on socket close / process exit - for { - n, err := reader.Read(buf) - if err != nil { - if !isNormalPtyError(err) { - log.Printf("Failed to read from pty: %s", err) - } - return - } - i, err := normalizeBuffer(&buffer, buf, n) - if err != nil { - log.Printf("Couldn't normalize byte buffer to UTF-8 sequence, due to an error: %s", err.Error()) - return - } - - if err := writeToSocket(conn, buffer.Bytes(), finalizer); err != nil { - return - } - - buffer.Reset() - if i < n { - buffer.Write(buf[i:n]) - } - } -} - -//we write message to websocket with help mutex finalizer to prevent send message after "close connection" message. -func writeToSocket(conn *websocket.Conn, bytes []byte, finalizer *ReadWriteRoutingFinalizer) error { - defer finalizer.Unlock() - - finalizer.Lock() - if err := conn.WriteMessage(websocket.TextMessage, bytes); err != nil { - log.Printf("Failed to send websocket message: %s, due to occurred error %s", string(bytes), err.Error()) - return err - } - return nil -} - -func ptyHandler(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Fatalf("Websocket upgrade failed: %s\n", err) - } - - wp, err := StartPty() - if err != nil { - log.Fatalf("Failed to start command: %s\n", err) - return - } - - reader := ioutil.NopCloser(wp.PtyFile) - finalizer := ReadWriteRoutingFinalizer{&sync.Mutex{}, false, false, false} - - defer waitAndClose(wp, &finalizer, conn, reader) - - //read output from terminal - go sendPtyOutputToConnection(conn, reader, &finalizer) - //write input to terminal - go sendConnectionInputToPty(conn, reader, wp, &finalizer) - - fmt.Println("New terminal successfully initialized.") -} - -func waitAndClose(wp *wsPty, finalizer *ReadWriteRoutingFinalizer, conn *websocket.Conn, reader io.ReadCloser) { - //ignore GIGHUP(hang up) error it's a normal signal to close terminal - if err := wp.Cmd.Wait(); err != nil && err.Error() != "signal: hangup" { - log.Printf("Failed to stop process, due to occurred error '%s'", err.Error()) - } - - closeFile(wp.PtyFile, finalizer) - closeConn(conn, finalizer) - closeReader(reader, wp.PtyFile, finalizer) - - fmt.Println("Terminal process completed.") -} - -func closeReader(reader io.ReadCloser, file *os.File, finalizer *ReadWriteRoutingFinalizer) { - defer finalizer.Unlock() - - finalizer.Lock() - if !finalizer.readDone { - closeReaderErr := reader.Close() - if closeReaderErr != nil { - log.Printf("Failed to close pty file reader: '%s'" + closeReaderErr.Error()) - } - //hack to prevent suspend reader on the operation read when file has been already closed. - file.Write([]byte{}) - finalizer.readDone = true - fmt.Println("Terminal reader closed.") - } - -} - -func closeConn(conn *websocket.Conn, finalizer *ReadWriteRoutingFinalizer) { - defer finalizer.Unlock() - - finalizer.Lock() - if !finalizer.writeDone { - //to cleanly close websocket connection, a client should send a close - //frame and wait for the server to close the connection. - err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - if err != nil { - log.Printf("Failed to send websocket close message: '%s'", err.Error()) - } - if err := conn.Close(); err != nil { - fmt.Printf("Close connection problem: '%s'", err.Error()) - } - - finalizer.writeDone = true - fmt.Println("Terminal writer closed.") - } -} - -func closeFile(file *os.File, finalizer *ReadWriteRoutingFinalizer) { - defer finalizer.Unlock() - - finalizer.Lock() - if !finalizer.fileClosed { - if err := file.Close(); err != nil { - log.Printf("Failed to close pty file: '%s'", err.Error()) - } - finalizer.fileClosed = true - fmt.Println("Pty file closed.") - } -} - -func ConnectToPtyHF(w http.ResponseWriter, r *http.Request, _ rest.Params) error { - ptyHandler(w, r) - return nil -} diff --git a/agents/go-agents/src/main/go/terminal-agent/main.go b/agents/go-agents/src/main/go/terminal-agent/main.go new file mode 100644 index 00000000000..a47a269689b --- /dev/null +++ b/agents/go-agents/src/main/go/terminal-agent/main.go @@ -0,0 +1,171 @@ +// +// 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 +// + +// websocket/pty proxy server: +// This program wires a websocket to a pty master. +// +// Usage: +// go build -o che-websocket-terminal +// ./che-websocket-terminal -cmd /bin/bash -addr :9000 +// +// Copyright 2014 Al Tobey tobert@gmail.com +// MIT License, see the LICENSE file +// +package main + +import ( + "flag" + "log" + "net/http" + "os" + "time" + + "github.com/eclipse/che/agents/go-agents/src/main/go/core/activity" + "github.com/eclipse/che/agents/go-agents/src/main/go/core/auth" + "github.com/eclipse/che/agents/go-agents/src/main/go/core/rest" + "github.com/eclipse/che/agents/go-agents/src/main/go/terminal-agent/term" +) + +var ( + serverAddress string + basePath string + apiEndpoint string + + workspaceID string + authEnabled bool + tokensExpirationTimeoutInMinutes uint +) + +func init() { + // server configuration + flag.StringVar( + &serverAddress, + "addr", + ":9000", + "IP:PORT or :PORT the address to start the server on", + ) + flag.StringVar( + &basePath, + "path", + "", + `the base path for all the rpc & rest routes, so route paths are treated not + as 'server_address + route_path' but 'server_address + path + route_path'. + For example for the server address 'localhost:9000', route path '/pty' and + configured path '/api/' terminal-agent server will serve the following route: + 'localhost:9000/api/pty'. + Regexp syntax is supported`, + ) + + // terminal configuration + flag.StringVar( + &term.Cmd, + "cmd", + "/bin/bash", + "shell interpreter and command to execute on slave side of the pty", + ) + flag.BoolVar( + &activity.ActivityTrackingEnabled, + "enable-activity-tracking", + false, + "whether workspace master will be notified about workspace activity", + ) + + // workspace master server configuration + flag.StringVar( + &apiEndpoint, + "api-endpoint", + os.Getenv("CHE_API"), + `api-endpoint used by terminal-agent modules(such as activity checker or authentication) + to request workspace master. By default the value from 'CHE_API' environment variable is used`, + ) + + // auth configuration + flag.BoolVar( + &authEnabled, + "enable-auth", + false, + "whether authenicate requests on workspace master before allowing them to proceed", + ) + flag.UintVar( + &tokensExpirationTimeoutInMinutes, + "tokens-expiration-timeout", + auth.DefaultTokensExpirationTimeoutInMinutes, + "how much time machine tokens stay in cache(if auth is enabled)", + ) + + workspaceID = os.Getenv("CHE_WORKSPACE_ID") +} + +func main() { + flag.Parse() + + log.SetOutput(os.Stdout) + + printConfiguration() + + if activity.ActivityTrackingEnabled { + activity.Tracker = activity.NewTracker(workspaceID, apiEndpoint) + go activity.Tracker.StartTracking() + } + + appHTTPRoutes := []rest.RoutesGroup{ + term.HTTPRoutes, + } + + // register routes and http handlers + r := rest.NewDefaultRouter(basePath, appHTTPRoutes) + rest.PrintRoutes(appHTTPRoutes) + + var handler = getHandler(r) + http.Handle("/", handler) + + server := &http.Server{ + Handler: handler, + Addr: serverAddress, + WriteTimeout: 10 * time.Second, + ReadTimeout: 10 * time.Second, + } + log.Fatal(server.ListenAndServe()) +} + +func getHandler(h http.Handler) http.Handler { + // required authentication for all the requests, if it is configured + if authEnabled { + cache := auth.NewCache(time.Minute*time.Duration(tokensExpirationTimeoutInMinutes), time.Minute*5) + return auth.NewCachingHandler(h, apiEndpoint, droppingTerminalConnectionsUnauthorizedHandler, cache) + } + + return h +} + +func droppingTerminalConnectionsUnauthorizedHandler(w http.ResponseWriter, req *http.Request, err error) { + // TODO disconnect all the clients with the same token if authentication returned unauthorized. +} + +func printConfiguration() { + log.Println("Terminal-agent configuration") + log.Println(" Server") + log.Printf(" - Address: %s\n", serverAddress) + log.Printf(" - Base path: '%s'\n", basePath) + log.Println(" Terminal") + log.Printf(" - Slave command: '%s'\n", term.Cmd) + log.Printf(" - Activity tracking enabled: %t\n", activity.ActivityTrackingEnabled) + if authEnabled { + log.Println(" Authentication") + log.Printf(" - Enabled: %t\n", authEnabled) + log.Printf(" - Tokens expiration timeout: %dm\n", tokensExpirationTimeoutInMinutes) + } + if authEnabled || activity.ActivityTrackingEnabled { + log.Println(" Workspace master server") + log.Printf(" - API endpoint: %s\n", apiEndpoint) + } + log.Println() +} diff --git a/agents/go-agents/src/main/go/terminal-agent/term/finalizer.go b/agents/go-agents/src/main/go/terminal-agent/term/finalizer.go new file mode 100644 index 00000000000..85d0b05b60f --- /dev/null +++ b/agents/go-agents/src/main/go/terminal-agent/term/finalizer.go @@ -0,0 +1,101 @@ +// +// 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 term + +import ( + "io" + "log" + "os" + "sync" + + "github.com/eclipse/che-lib/websocket" +) + +// readWriteRoutingFinalizer helps to close connection in concurrent environment corectly +type readWriteRoutingFinalizer struct { + sync.Mutex + readDone bool + writeDone bool + fileClosed bool + reader io.ReadCloser + conn *websocket.Conn + file *os.File +} + +func newFinalizer(reader io.ReadCloser, conn *websocket.Conn, file *os.File) *readWriteRoutingFinalizer { + return &readWriteRoutingFinalizer{ + readDone: false, + writeDone: false, + fileClosed: false, + reader: reader, + conn: conn, + file: file, + } +} + +func (finalizer *readWriteRoutingFinalizer) close() { + finalizer.closeFile() + finalizer.closeConn() + finalizer.closeReader() +} + +// It was supposed that reconnection can be implemented later. +// So if connection lost PTY file is not closed to allow reconnection. +func (finalizer *readWriteRoutingFinalizer) closeConn() { + defer finalizer.Unlock() + + finalizer.Lock() + if !finalizer.writeDone { + // to cleanly close websocket connection, a client should send a close + // frame and wait for the server to close the connection. + err := finalizer.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + log.Printf("Failed to send websocket close message: '%s'", err.Error()) + } + if err := finalizer.conn.Close(); err != nil { + log.Printf("Close connection problem: '%s'", err.Error()) + } + + finalizer.writeDone = true + log.Println("Terminal writer closed.") + } +} + +func (finalizer *readWriteRoutingFinalizer) closeFile() { + defer finalizer.Unlock() + + finalizer.Lock() + if !finalizer.fileClosed { + if err := finalizer.file.Close(); err != nil { + log.Printf("Failed to close pty file: '%s'", err.Error()) + } + finalizer.fileClosed = true + log.Println("Pty file closed.") + } +} + +func (finalizer *readWriteRoutingFinalizer) closeReader() { + defer finalizer.Unlock() + + finalizer.Lock() + if !finalizer.readDone { + closeReaderErr := finalizer.reader.Close() + if closeReaderErr != nil { + log.Printf("Failed to close pty file reader: '%s'" + closeReaderErr.Error()) + } + // hack to prevent suspend reader on the operation read when file has been already closed. + _, _ = finalizer.file.Write([]byte{}) + finalizer.readDone = true + log.Println("Terminal reader closed.") + } + +} diff --git a/agents/go-agents/src/main/go/terminal-agent/term/pty.go b/agents/go-agents/src/main/go/terminal-agent/term/pty.go new file mode 100644 index 00000000000..50f9a401eb6 --- /dev/null +++ b/agents/go-agents/src/main/go/terminal-agent/term/pty.go @@ -0,0 +1,142 @@ +// +// 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 term + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "io" + "log" + "os" + "os/exec" + "syscall" + "unicode/utf8" + + "github.com/eclipse/che-lib/pty" + "github.com/eclipse/che/agents/go-agents/src/main/go/core/activity" +) + +type wsPty struct { + cmd *exec.Cmd // pty builds on os.exec + ptyFile *os.File // a pty is simply an os.File +} + +// startPty starts shell interpreter and returns wsPty that represents this terminal +func startPty(command string) (*wsPty, error) { + cmd := exec.Command(command) + cmd.Env = append(os.Environ(), "TERM=xterm") + + file, err := pty.Start(cmd) + if err != nil { + return nil, err + } + + //Set the size of the pty + if err := pty.Setsize(file, 60, 200); err != nil { + log.Printf("Error occurs on setting terminal size. %s", err) + } + + return &wsPty{ + ptyFile: file, + cmd: cmd, + }, nil +} + +// Close stops terminal process and its child processes. In modern Unix systems terminal stops with help +// SIGHUP signal and we used such way too. SIGHUP signal used to send a signal to a process +// (or process group), it's signal meaning that pseudo or virtual terminal has been closed. +// Example: command is executed inside a terminal window and the terminal window is closed while +// the command process is still running. +// If the process receiving SIGHUP is a Unix shell, then as part of job control it will often intercept +// the signal and ensure that all stopped processes are continued before sending the signal to child +// processes (more precisely, process groups, represented internally be the shell as a "job"), which +// by default terminates them. +func (wp *wsPty) Close(finalizer *readWriteRoutingFinalizer) { + finalizer.closeFile() + pid := wp.cmd.Process.Pid + if pgid, err := syscall.Getpgid(pid); err == nil { + if err := syscall.Kill(-pgid, syscall.SIGHUP); err != nil { + log.Printf("Failed to SIGHUP terminal process by pgid: '%d'. Cause: '%s'", pgid, err) + } + } + if err := syscall.Kill(pid, syscall.SIGHUP); err != nil { + log.Printf("Failed to SIGHUP terminal process by pid '%d'. Cause: '%s'", pid, err) + } +} + +func (wp *wsPty) handleMessage(msg WebSocketMessage) error { + switch msg.Type { + case "resize": + var size []float64 + if err := json.Unmarshal(msg.Data, &size); err != nil { + log.Printf("Invalid resize message: %s\n", err) + } else { + if err := pty.Setsize(wp.ptyFile, uint16(size[1]), uint16(size[0])); err != nil { + log.Printf("Error occurs on setting terminal size. %s", err) + } + activity.Tracker.Notify() + } + + case "data": + var dat string + if err := json.Unmarshal(msg.Data, &dat); err != nil { + log.Printf("Invalid data message %s\n", err) + } else { + if _, err := wp.ptyFile.Write([]byte(dat)); err != nil { + log.Printf("Error occurs on writing data into terminal. %s", err) + } + activity.Tracker.Notify() + } + + default: + return errors.New("Invalid field message type: " + msg.Type + "\n") + } + return nil +} + +func isNormalPtyError(err error) bool { + if err == io.EOF { + return true + } + pathErr, ok := err.(*os.PathError) + return ok && + pathErr.Op == "read" && + pathErr.Path == "/dev/ptmx" && + pathErr.Err.Error() == "input/output error" +} + +// read byte array as Unicode code points (rune in go) +func normalizeBuffer(normalizedBuf *bytes.Buffer, buf []byte, n int) (int, error) { + bufferBytes := normalizedBuf.Bytes() + runeReader := bufio.NewReader(bytes.NewReader(append(bufferBytes[:], buf[:n]...))) + normalizedBuf.Reset() + i := 0 + for i < n { + char, charLen, err := runeReader.ReadRune() + if err != nil { + return i, err + } + if char == utf8.RuneError { + if err := runeReader.UnreadRune(); err != nil { + log.Print(err) + } + return i, nil + } + i += charLen + if _, err := normalizedBuf.WriteRune(char); err != nil { + return i, err + } + } + return i, nil +} diff --git a/agents/go-agents/src/main/go/terminal-agent/term/server.go b/agents/go-agents/src/main/go/terminal-agent/term/server.go new file mode 100644 index 00000000000..721ee92dc69 --- /dev/null +++ b/agents/go-agents/src/main/go/terminal-agent/term/server.go @@ -0,0 +1,179 @@ +// moved from https://github.com/eclipse/che-lib/tree/master/websocket-terminal + +package term + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "log" + "net/http" + "time" + + "github.com/eclipse/che-lib/websocket" + "github.com/eclipse/che/agents/go-agents/src/main/go/core/common" + "github.com/eclipse/che/agents/go-agents/src/main/go/core/rest" +) + +// WebSocketMessage represents message sent over websocket connection +type WebSocketMessage struct { + Type string `json:"type"` + Data json.RawMessage `json:"data"` +} + +var ( + upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + // Cmd is command used to start new shell + Cmd string + + // PingPeriod defines period of WS pings + PingPeriod = 60 * time.Second + + // HTTPRoutes provides http routes that should be handled to use terminal service + HTTPRoutes = rest.RoutesGroup{ + Name: "Terminal routes", + Items: []rest.Route{ + { + Method: "GET", + Name: "Connect to pty(webscoket)", + Path: "/pty", + HandleFunc: ConnectToPtyHF, + }, + }, + } +) + +// ConnectToPtyHF provides communication with TTY over websocket +func ConnectToPtyHF(w http.ResponseWriter, r *http.Request, _ rest.Params) error { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + // upgrader writes http error into response, so no need to process error + return nil + } + + wp, err := startPty(Cmd) + if err != nil { + sendInternalError(conn, "Failed to start command: "+err.Error()) + return nil + } + + reader := ioutil.NopCloser(wp.ptyFile) + finalizer := newFinalizer(reader, conn, wp.ptyFile) + + log.Println("Start new terminal.") + defer log.Println("Terminal process stopped.") + defer finalizer.close() + + // send ping messages + go setupWSPinging(conn, finalizer) + //read output from terminal + go sendPtyOutputToConnection(conn, reader, finalizer) + //write input to terminal + go sendConnectionInputToPty(conn, wp, finalizer) + + waitPTY(wp) + + return nil +} + +// read from the web socket, copying to the pty master +// messages are expected to be text and base64 encoded +func sendConnectionInputToPty(conn *websocket.Conn, wp *wsPty, finalizer *readWriteRoutingFinalizer) { + defer finalizer.closeReader() + + for { + mt, payload, err := conn.ReadMessage() + if err != nil { + if !isNormalWSError(err) { + log.Printf("conn.ReadMessage failed: %s\n", err) + } + return + } + switch mt { + case websocket.BinaryMessage: + log.Printf("Ignoring binary message: %q\n", payload) + case websocket.TextMessage: + var msg WebSocketMessage + if err := json.Unmarshal(payload, &msg); err != nil { + log.Printf("Invalid message %s\n", err) + continue + } + if msg.Type == "close" { + wp.Close(finalizer) + return + } + if errMsg := wp.handleMessage(msg); errMsg != nil { + log.Print(errMsg.Error()) + return + } + + default: + log.Printf("Invalid websocket message type %d\n", mt) + return + } + } +} + +// copy everything from the pty master to the websocket +// using base64 encoding for now due to limitations in term.js +func sendPtyOutputToConnection(conn *websocket.Conn, reader io.Reader, finalizer *readWriteRoutingFinalizer) { + defer finalizer.closeConn() + + buf := make([]byte, 8192) + var buffer bytes.Buffer + // TODO: more graceful exit on socket close / process exit + for { + n, err := reader.Read(buf) + if err != nil { + if !isNormalPtyError(err) { + log.Printf("Failed to read from pty: %s", err) + } + return + } + i, err := normalizeBuffer(&buffer, buf, n) + if err != nil { + log.Printf("Couldn't normalize byte buffer to UTF-8 sequence, due to an error: %s", err.Error()) + return + } + + if err := writeToSocket(conn, buffer.Bytes(), finalizer); err != nil { + return + } + + buffer.Reset() + if i < n { + buffer.Write(buf[i:n]) + } + } +} + +func setupWSPinging(conn *websocket.Conn, finalizer *readWriteRoutingFinalizer) { + ticker := time.NewTicker(PingPeriod) + defer ticker.Stop() + // send ping messages by sheduler + for range ticker.C { + if err := writeWSMessageToSocket(conn, websocket.PingMessage, []byte{}, finalizer); err != nil { + log.Printf("Error occurs on sending ping message to websocket. %v", err) + return + } + } +} + +func waitPTY(wp *wsPty) { + // ignore SIGHUP(hang up) error it's a normal signal to close terminal + if err := wp.cmd.Wait(); err != nil && err.Error() != "signal: hangup" { + log.Printf("Failed to stop process, due to occurred error '%s'", err.Error()) + } +} + +func sendInternalError(conn *websocket.Conn, err string) { + log.Println(err) + common.LogError(conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err))) + common.LogError(conn.Close()) +} diff --git a/agents/go-agents/src/main/go/terminal-agent/term/websocket.go b/agents/go-agents/src/main/go/terminal-agent/term/websocket.go new file mode 100644 index 00000000000..28e71cb80f8 --- /dev/null +++ b/agents/go-agents/src/main/go/terminal-agent/term/websocket.go @@ -0,0 +1,45 @@ +// +// 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 term + +import ( + "log" + "net" + + "github.com/eclipse/che-lib/websocket" +) + +func isNormalWSError(err error) bool { + closeErr, ok := err.(*websocket.CloseError) + if ok && (closeErr.Code == websocket.CloseGoingAway || closeErr.Code == websocket.CloseNormalClosure) { + return true + } + _, ok = err.(*net.OpError) + return ok +} + +// writeToSocket writes message of type TextMessage into websocket connection +func writeToSocket(conn *websocket.Conn, bytes []byte, finalizer *readWriteRoutingFinalizer) error { + return writeWSMessageToSocket(conn, websocket.TextMessage, bytes, finalizer) +} + +// we write message to websocket with help mutex finalizer to prevent send message after "close connection" message. +func writeWSMessageToSocket(conn *websocket.Conn, messageType int, bytes []byte, finalizer *readWriteRoutingFinalizer) error { + defer finalizer.Unlock() + + finalizer.Lock() + if err := conn.WriteMessage(messageType, bytes); err != nil { + log.Printf("Failed to send websocket message: %s, due to occurred error %s", string(bytes), err.Error()) + return err + } + return nil +} diff --git a/agents/pom.xml b/agents/pom.xml index 995a1d23a6b..0a0cd911c4b 100644 --- a/agents/pom.xml +++ b/agents/pom.xml @@ -19,13 +19,14 @@ 5.6.0-SNAPSHOT ../pom.xml - org.eclipse.che che-agents-parent 5.6.0-SNAPSHOT pom Che Agents Parent go-agents + exec + terminal ssh unison che-core-api-agent-shared diff --git a/agents/terminal/pom.xml b/agents/terminal/pom.xml new file mode 100644 index 00000000000..cd15952b4e5 --- /dev/null +++ b/agents/terminal/pom.xml @@ -0,0 +1,192 @@ + + + + 4.0.0 + + che-agents-parent + org.eclipse.che + 5.6.0-SNAPSHOT + + terminal-agent + Agent :: Terminal + + + com.google.inject + guice + + + javax.inject + javax.inject + + + org.eclipse.che.core + che-core-api-agent + + + org.eclipse.che.core + che-core-api-agent-shared + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-machine + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + copy-sources + compile + + run + + + + + + + + + + + + + + + + + com.soebes.maven.plugins + iterator-maven-plugin + 0.5.0 + + + compile-go-agents + compile + + iterator + + + + + linux_arm5 + + linux + arm + 5 + + + + linux_arm6 + + linux + arm + 6 + + + + linux_arm7 + + linux + arm + 7 + + + + linux_amd64 + + linux + amd64 + + + + linux_i386 + + linux + 386 + + + + + + + org.codehaus.mojo + exec-maven-plugin + + exec + + go + ${project.build.directory}/go-workspace/src/github.com/eclipse/che/agents/go-agents/src/main/go/terminal-agent + + build + -a + -installsuffix + cgo + -o + ${project.build.directory}/${item}/che-websocket-terminal + + + 0 + ${project.build.directory}/go-workspace + ${go.target.os} + ${go.target.architecture} + ${go.target.arm.version} + + + + + + + + assembly + package + + iterator + + + + linux_arm5 + linux_arm6 + linux_arm7 + linux_amd64 + linux_i386 + + + + + maven-assembly-plugin + + single + + posix + + ${basedir}/src/assembly/assembly.xml + + + + + + + + + + + diff --git a/agents/go-agents/src/assembly/assembly.xml b/agents/terminal/src/assembly/assembly.xml similarity index 100% rename from agents/go-agents/src/assembly/assembly.xml rename to agents/terminal/src/assembly/assembly.xml diff --git a/agents/terminal/src/main/java/org/eclipse/che/api/agent/TerminalAgent.java b/agents/terminal/src/main/java/org/eclipse/che/api/agent/TerminalAgent.java new file mode 100644 index 00000000000..d7de44a4929 --- /dev/null +++ b/agents/terminal/src/main/java/org/eclipse/che/api/agent/TerminalAgent.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * 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 com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.api.agent.shared.model.Agent; +import org.eclipse.che.api.agent.shared.model.impl.BasicAgent; + +import java.io.IOException; + +/** + * Terminal agent. + * + * @see Agent + * + * @author Garagatyi Alexander + */ +@Singleton +public class TerminalAgent extends BasicAgent { + private static final String AGENT_DESCRIPTOR = "org.eclipse.che.terminal.json"; + private static final String AGENT_SCRIPT = "org.eclipse.che.terminal.script.sh"; + + @Inject + public TerminalAgent() throws IOException { + super(AGENT_DESCRIPTOR, AGENT_SCRIPT); + } +} diff --git a/agents/terminal/src/main/java/org/eclipse/che/api/agent/TerminalAgentLauncher.java b/agents/terminal/src/main/java/org/eclipse/che/api/agent/TerminalAgentLauncher.java new file mode 100644 index 00000000000..ea4d69ddde7 --- /dev/null +++ b/agents/terminal/src/main/java/org/eclipse/che/api/agent/TerminalAgentLauncher.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * 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.launcher.AbstractAgentLauncher; +import org.eclipse.che.api.agent.server.launcher.ProcessIsLaunchedChecker; +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.ServerException; +import org.eclipse.che.api.machine.server.spi.Instance; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +/** + * Starts terminal agent. + * + * @author Garagatyi Alexander + */ +@Singleton +public class TerminalAgentLauncher extends AbstractAgentLauncher { + private final String runCommand; + + @Inject + public TerminalAgentLauncher(@Named("che.agent.dev.max_start_time_ms") long agentMaxStartTimeMs, + @Named("che.agent.dev.ping_delay_ms") long agentPingDelayMs, + @Named("machine.terminal_agent.run_command") String runCommand) { + super(agentMaxStartTimeMs, agentPingDelayMs, new ProcessIsLaunchedChecker("che-websocket-terminal")); + this.runCommand = runCommand; + } + + @Override + public void launch(Instance machine, Agent agent) throws ServerException { + final AgentImpl agentCopy = new AgentImpl(agent); + agentCopy.setScript(agent.getScript() + "\n" + runCommand); + super.launch(machine, agentCopy); + } + + @Override + public String getMachineType() { + return "docker"; + } + + @Override + public String getAgentId() { + return "org.eclipse.che.terminal"; + } +} diff --git a/agents/go-agents/src/main/resources/org.eclipse.che.terminal.json b/agents/terminal/src/main/resources/org.eclipse.che.terminal.json similarity index 100% rename from agents/go-agents/src/main/resources/org.eclipse.che.terminal.json rename to agents/terminal/src/main/resources/org.eclipse.che.terminal.json diff --git a/agents/go-agents/src/main/resources/org.eclipse.che.terminal.script.sh b/agents/terminal/src/main/resources/org.eclipse.che.terminal.script.sh similarity index 100% rename from agents/go-agents/src/main/resources/org.eclipse.che.terminal.script.sh rename to agents/terminal/src/main/resources/org.eclipse.che.terminal.script.sh diff --git a/assembly/assembly-main/pom.xml b/assembly/assembly-main/pom.xml index 52d8de14b85..c372d599bec 100644 --- a/assembly/assembly-main/pom.xml +++ b/assembly/assembly-main/pom.xml @@ -39,15 +39,15 @@ org.eclipse.che - go-agents + exec-agent tar.gz linux_amd64 org.eclipse.che - go-agents + terminal-agent tar.gz - linux_arm7 + linux_amd64 org.eclipse.che.core diff --git a/assembly/assembly-main/src/assembly/assembly.xml b/assembly/assembly-main/src/assembly/assembly.xml index f60a8927c23..ce0ffe07d75 100644 --- a/assembly/assembly-main/src/assembly/assembly.xml +++ b/assembly/assembly-main/src/assembly/assembly.xml @@ -79,16 +79,16 @@ lib/linux_amd64/terminal websocket-terminal-linux_amd64.tar.gz - org.eclipse.che:go-agents:tar.gz:linux_amd64 + org.eclipse.che:terminal-agent:tar.gz:linux_amd64 false false - lib/linux_arm7/terminal - websocket-terminal-linux_arm7.tar.gz + lib/linux_amd64/exec + exec-agent-linux_amd64.tar.gz - org.eclipse.che:go-agents:tar.gz:linux_arm7 + org.eclipse.che:exec-agent:tar.gz:linux_amd64 diff --git a/assembly/assembly-main/src/assembly/stack/che-in-che.json b/assembly/assembly-main/src/assembly/stack/che-in-che.json index 915e73f9925..cefd568b94b 100644 --- a/assembly/assembly-main/src/assembly/stack/che-in-che.json +++ b/assembly/assembly-main/src/assembly/stack/che-in-che.json @@ -36,7 +36,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml index e21e599454f..9805ea7ff44 100644 --- a/assembly/assembly-wsmaster-war/pom.xml +++ b/assembly/assembly-wsmaster-war/pom.xml @@ -60,7 +60,7 @@ org.eclipse.che - go-agents + exec-agent org.eclipse.che @@ -86,6 +86,10 @@ org.eclipse.che ssh-agent + + org.eclipse.che + terminal-agent + org.eclipse.che unison-agent diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java index 5711bada446..981ad4e1cba 100644 --- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java +++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java @@ -14,8 +14,6 @@ import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Names; -import org.eclipse.che.api.agent.ExecAgent; -import org.eclipse.che.api.agent.ExecAgentLauncher; import org.eclipse.che.api.agent.LSCSharpAgent; import org.eclipse.che.api.agent.LSJsonAgent; import org.eclipse.che.api.agent.LSPhpAgent; @@ -124,7 +122,8 @@ protected void configure() { Multibinder agents = Multibinder.newSetBinder(binder(), Agent.class); agents.addBinding().to(SshAgent.class); agents.addBinding().to(UnisonAgent.class); - agents.addBinding().to(ExecAgent.class); + agents.addBinding().to(org.eclipse.che.api.agent.ExecAgent.class); + agents.addBinding().to(org.eclipse.che.api.agent.TerminalAgent.class); agents.addBinding().to(WsAgent.class); agents.addBinding().to(LSPhpAgent.class); agents.addBinding().to(LSPythonAgent.class); @@ -134,7 +133,8 @@ protected void configure() { Multibinder launchers = Multibinder.newSetBinder(binder(), AgentLauncher.class); launchers.addBinding().to(WsAgentLauncher.class); - launchers.addBinding().to(ExecAgentLauncher.class); + launchers.addBinding().to(org.eclipse.che.api.agent.ExecAgentLauncher.class); + launchers.addBinding().to(org.eclipse.che.api.agent.TerminalAgentLauncher.class); launchers.addBinding().to(SshAgentLauncher.class); bindConstant().annotatedWith(Names.named("machine.ws_agent.run_command")) @@ -142,8 +142,11 @@ protected void configure() { bindConstant().annotatedWith(Names.named("machine.terminal_agent.run_command")) .to("$HOME/che/terminal/che-websocket-terminal " + "-addr :4411 " + + "-cmd ${SHELL_INTERPRETER}"); + bindConstant().annotatedWith(Names.named("machine.exec_agent.run_command")) + .to("$HOME/che/exec-agent/che-exec-agent " + + "-addr :4412 " + "-cmd ${SHELL_INTERPRETER} " + - "-static $HOME/che/terminal/ " + "-logs-dir $HOME/che/exec-agent/logs"); bind(org.eclipse.che.api.deploy.WsMasterAnalyticsAddresser.class); diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/che.properties index 59bc43b6160..08ef8db9019 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/che.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/che.properties @@ -81,6 +81,8 @@ che.workspace.ssh_connection_timeout_ms=10000 che.workspace.terminal_linux_amd64=${che.home}/lib/linux_amd64/terminal che.workspace.terminal_linux_arm7=${che.home}/lib/linux_arm7/terminal +che.workspace.exec_linux_amd64=${che.home}/lib/linux_amd64/exec + # Folder where the workspace will store logs from agents and other runtimes che.workspace.logs=${che.logs.dir}/machine/logs diff --git a/dashboard/e2e/stacks/list-stacks/list-stack.mock.js b/dashboard/e2e/stacks/list-stacks/list-stack.mock.js index b9eca943ec9..bd2cdf7053a 100644 --- a/dashboard/e2e/stacks/list-stacks/list-stack.mock.js +++ b/dashboard/e2e/stacks/list-stacks/list-stack.mock.js @@ -26,7 +26,7 @@ exports.listStacksTheeEntries = function () { 'machines': { 'dev-machine': { 'servers': {}, - 'agents': ['org.eclipse.che.terminal', 'org.eclipse.che.ws-agent', 'org.eclipse.che.ssh'], + 'agents': ['org.eclipse.che.exec', 'org.eclipse.che.terminal', 'org.eclipse.che.ws-agent', 'org.eclipse.che.ssh'], 'attributes': {'memoryLimitBytes': '2147483648'} } }, 'recipe': {'location': 'codenvy/node', 'type': 'dockerimage'} diff --git a/dashboard/e2e/stacks/stack-details/stack-details.mock.js b/dashboard/e2e/stacks/stack-details/stack-details.mock.js index e68b2eec3ad..42a6edfac7b 100644 --- a/dashboard/e2e/stacks/stack-details/stack-details.mock.js +++ b/dashboard/e2e/stacks/stack-details/stack-details.mock.js @@ -23,7 +23,7 @@ exports.dockerimageStack = function () { 'machines': { 'dev-machine': { 'servers': {}, - 'agents': ['org.eclipse.che.terminal', 'org.eclipse.che.ws-agent', 'org.eclipse.che.ssh'], + 'agents': ['org.eclipse.che.exec', 'org.eclipse.che.terminal', 'org.eclipse.che.ws-agent', 'org.eclipse.che.ssh'], 'attributes': {'memoryLimitBytes': '2147483648'} } }, 'recipe': {'location': 'eclipse/node', 'type': 'dockerimage'} @@ -60,7 +60,7 @@ exports.dockerfileStack = function () { 'machines': { 'dev-machine': { 'servers': {}, - 'agents': ['org.eclipse.che.ws-agent', 'org.eclipse.che.ssh', 'org.eclipse.che.terminal'], + 'agents': ['org.eclipse.che.ws-agent', 'org.eclipse.che.ssh', 'org.eclipse.che.exec', 'org.eclipse.che.terminal'], 'attributes': {'memoryLimitBytes': '2147483648'} } } @@ -98,6 +98,7 @@ exports.composefileStack = function () { 'db': { 'servers': {}, 'agents': [ + 'org.eclipse.che.exec', 'org.eclipse.che.terminal' ], 'attributes': { @@ -107,6 +108,7 @@ exports.composefileStack = function () { 'dev-machine': { 'servers': {}, 'agents': [ + 'org.eclipse.che.exec', 'org.eclipse.che.terminal', 'org.eclipse.che.ws-agent', 'org.eclipse.che.ssh' diff --git a/dashboard/src/app/workspaces/workspace-details/environments/add-machine-dialog/add-machine-dialog.controller.ts b/dashboard/src/app/workspaces/workspace-details/environments/add-machine-dialog/add-machine-dialog.controller.ts index d46558ed2ac..a40bc7a326d 100644 --- a/dashboard/src/app/workspaces/workspace-details/environments/add-machine-dialog/add-machine-dialog.controller.ts +++ b/dashboard/src/app/workspaces/workspace-details/environments/add-machine-dialog/add-machine-dialog.controller.ts @@ -74,7 +74,7 @@ export class AddMachineDialogController { setDefaultData(): void { this.machine = { 'agents': [ - 'org.eclipse.che.terminal', 'org.eclipse.che.ssh' + 'org.eclipse.che.exec', 'org.eclipse.che.terminal', 'org.eclipse.che.ssh' ], 'servers': {}, 'attributes': {} diff --git a/dashboard/src/components/api/che-stack.factory.ts b/dashboard/src/components/api/che-stack.factory.ts index 076f1ea6a2b..9f7688c5178 100644 --- a/dashboard/src/components/api/che-stack.factory.ts +++ b/dashboard/src/components/api/che-stack.factory.ts @@ -78,7 +78,7 @@ export class CheStack { 'machines': { 'dev-machine': { 'agents': [ - 'org.eclipse.che.terminal', 'org.eclipse.che.ws-agent', 'org.eclipse.che.ssh' + 'org.eclipse.che.exec', 'org.eclipse.che.terminal', 'org.eclipse.che.ws-agent', 'org.eclipse.che.ssh' ], 'servers': {}, 'attributes': { diff --git a/dashboard/src/components/api/che-workspace.factory.ts b/dashboard/src/components/api/che-workspace.factory.ts index f614a115a4a..c7fd0b025ca 100644 --- a/dashboard/src/components/api/che-workspace.factory.ts +++ b/dashboard/src/components/api/che-workspace.factory.ts @@ -322,7 +322,7 @@ export class CheWorkspace { 'machines': { 'dev-machine': { 'attributes': {'memoryLimitBytes': ram}, - 'agents': ['org.eclipse.che.ws-agent', 'org.eclipse.che.terminal', 'org.eclipse.che.ssh'] + 'agents': ['org.eclipse.che.ws-agent', 'org.eclipse.che.exec', 'org.eclipse.che.terminal', 'org.eclipse.che.ssh'] } } }; @@ -355,7 +355,7 @@ export class CheWorkspace { 'name': 'ws-machine', 'attributes': {'memoryLimitBytes': ram}, 'type': 'docker', - 'agents': ['org.eclipse.che.ws-agent', 'org.eclipse.che.terminal', 'org.eclipse.che.ssh'] + 'agents': ['org.eclipse.che.ws-agent', 'org.eclipse.che.exec', 'org.eclipse.che.terminal', 'org.eclipse.che.ssh'] }; defaultEnvironment.machines[devMachine.name] = devMachine; } else { diff --git a/dockerfiles/che/entrypoint.sh b/dockerfiles/che/entrypoint.sh index 43441a6df3a..9d167ab2859 100755 --- a/dockerfiles/che/entrypoint.sh +++ b/dockerfiles/che/entrypoint.sh @@ -266,6 +266,7 @@ init() { sed -i "/che.workspace.agent.dev=/c\che.workspace.agent.dev=${CHE_DATA_HOST}/lib/ws-agent.tar.gz" $CHE_LOCAL_CONF_DIR/che.properties sed -i "/che.workspace.terminal_linux_amd64=/c\che.workspace.terminal_linux_amd64=${CHE_DATA_HOST}/lib/linux_amd64/terminal" $CHE_LOCAL_CONF_DIR/che.properties sed -i "/che.workspace.terminal_linux_arm7=/c\che.workspace.terminal_linux_arm7=${CHE_DATA_HOST}/lib/linux_arm7/terminal" $CHE_LOCAL_CONF_DIR/che.properties + sed -i "/che.workspace.exec_linux_amd64=/c\che.workspace.exec_linux_amd64=${CHE_DATA_HOST}/lib/linux_amd64/exec" $CHE_LOCAL_CONF_DIR/che.properties # CHE_DOCKER_IP_EXTERNAL must be set if you are in a VM. HOSTNAME=${CHE_DOCKER_IP_EXTERNAL:-$(get_docker_external_hostname)} diff --git a/dockerfiles/lib/src/api/wsmaster/workspace/workspace.ts b/dockerfiles/lib/src/api/wsmaster/workspace/workspace.ts index 793c684b1e8..2ecfee8cc82 100644 --- a/dockerfiles/lib/src/api/wsmaster/workspace/workspace.ts +++ b/dockerfiles/lib/src/api/wsmaster/workspace/workspace.ts @@ -63,6 +63,7 @@ export class Workspace { */ getWorkspaceConfigDto(createWorkspaceConfig:CreateWorkspaceConfig) : org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto { let devMachine : org.eclipse.che.api.workspace.shared.dto.ExtendedMachineDto = new org.eclipse.che.api.workspace.shared.dto.ExtendedMachineDtoImpl(); + devMachine.getAgents().push("org.eclipse.che.exec"); devMachine.getAgents().push("org.eclipse.che.terminal"); devMachine.getAgents().push("org.eclipse.che.ws-agent"); devMachine.getAgents().push("org.eclipse.che.ssh"); diff --git a/dockerfiles/lib/src/internal/action/impl/execute-command-action.ts b/dockerfiles/lib/src/internal/action/impl/execute-command-action.ts index 9f3e568464c..0e9838724a7 100644 --- a/dockerfiles/lib/src/internal/action/impl/execute-command-action.ts +++ b/dockerfiles/lib/src/internal/action/impl/execute-command-action.ts @@ -68,8 +68,8 @@ export class ExecuteCommandAction { // get dev machine let machineId : string = workspaceDto.getRuntime().getDevMachine().getId(); - // get terminal URI - let execAgentServer = workspaceDto.getRuntime().getDevMachine().getRuntime().getServers().get("4411/tcp"); + // get exec-agent URI + let execAgentServer = workspaceDto.getRuntime().getDevMachine().getRuntime().getServers().get("4412/tcp"); let execAgentURI = execAgentServer.getUrl(); if (execAgentURI.includes("localhost")) { execAgentURI = execAgentServer.getProperties().getInternalUrl(); diff --git a/dockerfiles/lib/src/internal/dir/che-dir.ts b/dockerfiles/lib/src/internal/dir/che-dir.ts index bee5b85365d..0c1f06ec4d6 100644 --- a/dockerfiles/lib/src/internal/dir/che-dir.ts +++ b/dockerfiles/lib/src/internal/dir/che-dir.ts @@ -720,7 +720,7 @@ setupSSHKeys(workspaceDto: org.eclipse.che.api.workspace.shared.dto.WorkspaceDto let machineId : string = workspaceDto.getRuntime().getDevMachine().getId(); - let execAgentServer = workspaceDto.getRuntime().getDevMachine().getRuntime().getServers().get("4411/tcp"); + let execAgentServer = workspaceDto.getRuntime().getDevMachine().getRuntime().getServers().get("4412/tcp"); let execAgentURI = execAgentServer.getUrl(); if (execAgentURI.includes("localhost")) { execAgentURI = execAgentServer.getProperties().getInternalUrl(); @@ -846,8 +846,8 @@ setupSSHKeys(workspaceDto: org.eclipse.che.api.workspace.shared.dto.WorkspaceDto let promises : Array> = new Array>(); let workspaceCommands : Array = workspaceDto.getConfig().getCommands(); - // get terminal URI - let execAgentServer = workspaceDto.getRuntime().getDevMachine().getRuntime().getServers().get("4411/tcp"); + // get exec-agent URI + let execAgentServer = workspaceDto.getRuntime().getDevMachine().getRuntime().getServers().get("4412/tcp"); let execAgentURI = execAgentServer.getUrl(); if (execAgentURI.includes("localhost")) { execAgentURI = execAgentServer.getProperties().getInternalUrl(); diff --git a/ide/che-core-ide-stacks/src/main/resources/stacks.json b/ide/che-core-ide-stacks/src/main/resources/stacks.json index cfa7abcded2..2850f50bde5 100644 --- a/ide/che-core-ide-stacks/src/main/resources/stacks.json +++ b/ide/che-core-ide-stacks/src/main/resources/stacks.json @@ -32,14 +32,14 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : {} }, "db": { "agents": [ - "org.eclipse.che.terminal" + "org.eclipse.che.exec", "org.eclipse.che.terminal" ], "servers": {}, "attributes" : {} @@ -107,7 +107,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -160,7 +160,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -218,7 +218,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -285,7 +285,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -346,7 +346,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -417,7 +417,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -486,7 +486,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -563,7 +563,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -633,7 +633,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" ], "servers": {}, "attributes" : { @@ -730,7 +730,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -800,7 +800,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -866,7 +866,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -911,6 +911,7 @@ "machines": { "dev-machine": { "agents": [ + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", @@ -977,7 +978,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1049,7 +1050,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1124,7 +1125,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" ], "servers": {}, "attributes" : { @@ -1189,7 +1190,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1251,7 +1252,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1332,7 +1333,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1384,7 +1385,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1435,7 +1436,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1499,7 +1500,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1548,7 +1549,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1605,7 +1606,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" ], "servers": {}, "attributes" : { @@ -1662,7 +1663,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1719,7 +1720,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" ], "servers": {}, "attributes" : { @@ -1776,7 +1777,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" ], "servers": {}, "attributes" : { @@ -1833,7 +1834,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -1895,7 +1896,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh", "org.eclipse.che.ls.php", "org.eclipse.che.ls.json" ], "servers": {}, "attributes" : { @@ -1997,7 +1998,7 @@ "machines": { "dev-machine": { "agents": [ - "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" ], "servers": {}, "attributes" : { @@ -2099,6 +2100,7 @@ "machines": { "dev-machine": { "agents": [ + "org.eclipse.che.exec", "org.eclipse.che.terminal", "org.eclipse.che.ws-agent", "org.eclipse.che.ssh" diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ext/DockerMachineTerminalChecker.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ext/DockerMachineTerminalChecker.java index 32b9e93fdec..2c932f0cc4f 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ext/DockerMachineTerminalChecker.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ext/DockerMachineTerminalChecker.java @@ -27,6 +27,7 @@ */ public class DockerMachineTerminalChecker { + // TODO check exec agent binaries if needed public static final String TERMINAL_ARCHIVE_LOCATION = "che.workspace.terminal_linux_amd64"; private static final Logger LOG = LoggerFactory.getLogger(DockerMachineTerminalChecker.class); diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ext/provider/ExecAgentVolumeProvider.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ext/provider/ExecAgentVolumeProvider.java new file mode 100644 index 00000000000..bc1be1ef1b7 --- /dev/null +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/ext/provider/ExecAgentVolumeProvider.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * 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.docker.machine.ext.provider; + +import com.google.common.base.Strings; + +import org.eclipse.che.api.core.util.SystemInfo; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.commons.lang.IoUtil; +import org.eclipse.che.plugin.docker.machine.WindowsHostUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Provides volumes configuration of machine for exec agent + * + *

On Windows MUST be locate in "user.home" directory in case limitation windows+docker. + * + * @author Alexander Garagatyi + */ +@Singleton +public class ExecAgentVolumeProvider implements Provider { + + private static final String CONTAINER_TARGET = ":/mnt/che/exec-agent"; + private static final String EXEC = "exec"; + private static final Logger LOG = LoggerFactory.getLogger(ExecAgentVolumeProvider.class); + + private final String execArchivePath; + + private final String agentVolumeOptions; + + @Inject + public ExecAgentVolumeProvider(@Nullable @Named("che.docker.volumes_agent_options") String agentVolumeOptions, + @Named("che.workspace.exec_linux_amd64") String execArchivePath) { + if (!Strings.isNullOrEmpty(agentVolumeOptions)) { + this.agentVolumeOptions = ":" + agentVolumeOptions; + } else { + this.agentVolumeOptions = ""; + } + this.execArchivePath = execArchivePath; + } + + @Override + public String get() { + if (SystemInfo.isWindows()) { + try { + final Path cheHome = WindowsHostUtils.ensureCheHomeExist(); + final Path execPath = cheHome.resolve(EXEC); + IoUtil.copy(Paths.get(execArchivePath).toFile(), execPath.toFile(), null, true); + return getTargetOptions(execPath.toString()); + } catch (IOException e) { + LOG.warn(e.getMessage()); + throw new RuntimeException(e); + } + } else { + return getTargetOptions(execArchivePath); + } + } + + private String getTargetOptions(final String path) { + return path + CONTAINER_TARGET + agentVolumeOptions; + } + +} diff --git a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/local/LocalCheInfrastructureProvisioner.java b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/local/LocalCheInfrastructureProvisioner.java index b299bcba819..d756855ccb3 100644 --- a/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/local/LocalCheInfrastructureProvisioner.java +++ b/plugins/plugin-docker/che-plugin-docker-machine/src/main/java/org/eclipse/che/plugin/docker/machine/local/LocalCheInfrastructureProvisioner.java @@ -24,6 +24,7 @@ import org.eclipse.che.commons.lang.os.WindowsPathEscaper; import org.eclipse.che.inject.CheBootstrap; import org.eclipse.che.plugin.docker.machine.ext.provider.DockerExtConfBindingProvider; +import org.eclipse.che.plugin.docker.machine.ext.provider.ExecAgentVolumeProvider; import org.eclipse.che.plugin.docker.machine.ext.provider.TerminalVolumeProvider; import org.eclipse.che.plugin.docker.machine.ext.provider.WsAgentVolumeProvider; import org.eclipse.che.plugin.docker.machine.node.WorkspaceFolderPathProvider; @@ -50,6 +51,7 @@ public class LocalCheInfrastructureProvisioner extends DefaultInfrastructureProv private final WsAgentVolumeProvider wsAgentVolumeProvider; private final DockerExtConfBindingProvider dockerExtConfBindingProvider; private final TerminalVolumeProvider terminalVolumeProvider; + private final ExecAgentVolumeProvider execVolumeProvider; private final String projectsVolumeOptions; @Inject @@ -60,7 +62,8 @@ public LocalCheInfrastructureProvisioner(AgentConfigApplier agentConfigApplier, @Nullable @Named("che.docker.volumes_projects_options") String projectsVolumeOptions, WsAgentVolumeProvider wsAgentVolumeProvider, DockerExtConfBindingProvider dockerExtConfBindingProvider, - TerminalVolumeProvider terminalVolumeProvider) { + TerminalVolumeProvider terminalVolumeProvider, + ExecAgentVolumeProvider execAgentVolumeProvider) { super(agentConfigApplier); this.workspaceFolderPathProvider = workspaceFolderPathProvider; this.pathEscaper = pathEscaper; @@ -68,6 +71,7 @@ public LocalCheInfrastructureProvisioner(AgentConfigApplier agentConfigApplier, this.wsAgentVolumeProvider = wsAgentVolumeProvider; this.dockerExtConfBindingProvider = dockerExtConfBindingProvider; this.terminalVolumeProvider = terminalVolumeProvider; + this.execVolumeProvider = execAgentVolumeProvider; if (!Strings.isNullOrEmpty(projectsVolumeOptions)) { this.projectsVolumeOptions = ":" + projectsVolumeOptions; } else { @@ -88,6 +92,7 @@ public void provision(EnvironmentImpl envConfig, CheServicesEnvironmentImpl inte for (CheServiceImpl machine : internalEnv.getServices().values()) { ArrayList volumes = new ArrayList<>(machine.getVolumes()); volumes.add(terminalVolumeProvider.get()); + volumes.add(execVolumeProvider.get()); machine.setVolumes(volumes); } @@ -122,7 +127,9 @@ public void provision(EnvironmentImpl envConfig, CheServicesEnvironmentImpl inte @Override public void provision(ExtendedMachineImpl machineConfig, CheServiceImpl internalMachine) throws EnvironmentException { - internalMachine.getVolumes().add(terminalVolumeProvider.get()); + List volumes = internalMachine.getVolumes(); + volumes.add(terminalVolumeProvider.get()); + volumes.add(execVolumeProvider.get()); super.provision(machineConfig, internalMachine); } diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/CheServicePorts.java b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/CheServicePorts.java index ecf06b5b8b8..5fbbf8cee13 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/CheServicePorts.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/main/java/org/eclipse/che/plugin/openshift/client/CheServicePorts.java @@ -10,9 +10,10 @@ *******************************************************************************/ package org.eclipse.che.plugin.openshift.client; -import java.util.Map; import com.google.common.collect.ImmutableMap; +import java.util.Map; + /** * Provides mapping between port and Che service name that is using it */ @@ -22,6 +23,7 @@ public final class CheServicePorts { put(4401, "wsagent"). put(4403, "wsagent-jpda"). put(4411, "terminal"). + put(4412, "exec-agent"). put(8080, "tomcat"). put(8000, "tomcat-jpda"). put(9876, "codeserver").build(); diff --git a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesServiceTest.java b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesServiceTest.java index 8305c3e344f..a1b575415b0 100644 --- a/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesServiceTest.java +++ b/plugins/plugin-docker/che-plugin-openshift-client/src/test/java/org/eclipse/che/plugin/openshift/client/kubernetes/KubernetesServiceTest.java @@ -10,7 +10,10 @@ *******************************************************************************/ package org.eclipse.che.plugin.openshift.client.kubernetes; -import static org.testng.Assert.assertTrue; +import io.fabric8.kubernetes.api.model.ServicePort; + +import org.eclipse.che.plugin.docker.client.json.ExposedPort; +import org.testng.annotations.Test; import java.util.HashMap; import java.util.HashSet; @@ -19,11 +22,7 @@ import java.util.Set; import java.util.stream.Collectors; -import org.eclipse.che.plugin.docker.client.json.ExposedPort; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; - -import io.fabric8.kubernetes.api.model.ServicePort; +import static org.testng.Assert.assertTrue; public class KubernetesServiceTest { @@ -72,6 +71,7 @@ public void shouldReturnServicePortNameWhenKnownPortNumberIsProvided() { exposedPorts.put("4401/tcp",null); exposedPorts.put("4403/tcp",null); exposedPorts.put("4411/tcp",null); + exposedPorts.put("4412/tcp",null); exposedPorts.put("8080/tcp",null); exposedPorts.put("8888/tcp",null); exposedPorts.put("9876/tcp",null); @@ -81,6 +81,7 @@ public void shouldReturnServicePortNameWhenKnownPortNumberIsProvided() { expectedPortNames.add("wsagent"); expectedPortNames.add("wsagent-pda"); expectedPortNames.add("terminal"); + expectedPortNames.add("exec-agent"); expectedPortNames.add("tomcat"); expectedPortNames.add("tomcat-jpda"); expectedPortNames.add("codeserver"); diff --git a/plugins/plugin-terminal-ui/pom.xml b/plugins/plugin-terminal-ui/pom.xml index bd2d44fb6df..8e5508ad6d5 100644 --- a/plugins/plugin-terminal-ui/pom.xml +++ b/plugins/plugin-terminal-ui/pom.xml @@ -47,15 +47,15 @@ false - **/src/addons/** - **/src/handlers/** - **/src/main/resources/org/eclipse/che/ide/terminal/client/xterm.css - **/utils/** - **/src/Viewport.ts - **/src/CompositionHelper.ts - **/src/EventEmitter.js - **/src/xterm.js - **/src/Interfaces.ts + **/src/addons/** + **/src/handlers/** + **/src/main/resources/org/eclipse/che/ide/terminal/client/xterm.css + **/utils/** + **/src/Viewport.ts + **/src/CompositionHelper.ts + **/src/EventEmitter.js + **/src/xterm.js + **/src/Interfaces.ts @@ -97,19 +97,19 @@ - - copy-xterm.js - process-classes - - run - - - - - - - - + + copy-xterm.js + process-classes + + run + + + + + + + + diff --git a/plugins/plugin-testing-java/plugin-testing-junit/che-plugin-testing-junit-server/pom.xml b/plugins/plugin-testing-java/plugin-testing-junit/che-plugin-testing-junit-server/pom.xml index 8e48ea6aa84..38197c10d6b 100644 --- a/plugins/plugin-testing-java/plugin-testing-junit/che-plugin-testing-junit-server/pom.xml +++ b/plugins/plugin-testing-java/plugin-testing-junit/che-plugin-testing-junit-server/pom.xml @@ -66,8 +66,8 @@ javassist - org.slf4j - slf4j-api + org.slf4j + slf4j-api diff --git a/pom.xml b/pom.xml index 5a2df0f6436..13ef04d541d 100644 --- a/pom.xml +++ b/pom.xml @@ -95,22 +95,15 @@ org.eclipse.che - go-agents + exec-agent ${project.version} - tar.gz - linux_amd64 org.eclipse.che - go-agents + exec-agent ${project.version} tar.gz - linux_arm7 - - - org.eclipse.che - go-agents - ${project.version} + linux_amd64 org.eclipse.che @@ -142,6 +135,18 @@ ssh-agent ${project.version} + + org.eclipse.che + terminal-agent + ${project.version} + + + org.eclipse.che + terminal-agent + ${project.version} + tar.gz + linux_amd64 + org.eclipse.che unison-agent diff --git a/wsagent/agent/src/main/resources/org.eclipse.che.ws-agent.json b/wsagent/agent/src/main/resources/org.eclipse.che.ws-agent.json index 2dad37a3b66..91619216a5d 100644 --- a/wsagent/agent/src/main/resources/org.eclipse.che.ws-agent.json +++ b/wsagent/agent/src/main/resources/org.eclipse.che.ws-agent.json @@ -3,6 +3,7 @@ "name": "Workspace API", "description": "Workspace API support", "dependencies": [ + "org.eclipse.che.exec", "org.eclipse.che.terminal" ], "properties": {}, diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryService.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryService.java index 4482d279fc4..2f80426110a 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryService.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryService.java @@ -20,6 +20,7 @@ import com.google.gson.JsonSyntaxException; import org.apache.commons.fileupload.FileItem; +import org.eclipse.che.api.agent.server.filters.AddExecAgentInEnvironmentUtil; import org.eclipse.che.api.core.ApiException; import org.eclipse.che.api.core.BadRequestException; import org.eclipse.che.api.core.ConflictException; @@ -183,6 +184,7 @@ public FactoryDto saveFactory(Iterator formData) throws ForbiddenExcep } requiredNotNull(factory, "factory configuration"); processDefaults(factory); + AddExecAgentInEnvironmentUtil.addExecAgent(factory.getWorkspace()); createValidator.validateOnCreate(factory); return injectLinks(asDto(factoryManager.saveFactory(factory, images)), images); } catch (IOException ioEx) { diff --git a/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/FactoryServiceTest.java b/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/FactoryServiceTest.java index 617bc3a5380..40de4f11968 100644 --- a/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/FactoryServiceTest.java +++ b/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/FactoryServiceTest.java @@ -47,6 +47,7 @@ import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; import org.eclipse.che.api.workspace.shared.dto.EnvironmentDto; +import org.eclipse.che.api.workspace.shared.dto.ExtendedMachineDto; import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; import org.eclipse.che.api.workspace.shared.dto.SourceStorageDto; import org.eclipse.che.commons.env.EnvironmentContext; @@ -85,6 +86,7 @@ import static java.lang.String.format; import static java.lang.String.valueOf; import static java.lang.Thread.currentThread; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; @@ -95,6 +97,8 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static org.eclipse.che.api.factory.server.DtoConverter.asDto; import static org.eclipse.che.api.factory.server.FactoryService.VALIDATE_QUERY_PARAMETER; +import static org.eclipse.che.dto.server.DtoFactory.cloneDto; +import static org.eclipse.che.dto.server.DtoFactory.newDto; import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME; import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD; import static org.hamcrest.Matchers.equalTo; @@ -745,6 +749,68 @@ public void checkValidateResolver() throws Exception { verify(acceptValidator).validateOnAccept(any()); } + @Test + public void shouldAddExecAgentOnSaveFactoryFromFormData() throws Exception { + final Factory factory = createFactory(); + final FactoryDto factoryDto = asDto(factory, user); + + EnvironmentDto environment = newDto(EnvironmentDto.class); + ExtendedMachineDto machine = newDto(ExtendedMachineDto.class); + factoryDto.getWorkspace().setEnvironments(ImmutableMap.of("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ), + "e2", cloneDto(environment).withMachines( + ImmutableMap.of("m4", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json")), + "m5", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ))); + Map expectedEnvs = ImmutableMap.of("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ), + "e2", cloneDto(environment).withMachines( + ImmutableMap.of("m4", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m5", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + )); + + when(factoryManager.saveFactory(any(FactoryDto.class), + anySetOf(FactoryImage.class))) + .thenAnswer(invocation -> new FactoryImpl((Factory)invocation.getArguments()[0], null)); + doReturn(factoryDto).when(factoryBuilderSpy).build(any(InputStream.class)); + + final Response response = given().auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .multiPart("factory", JsonHelper.toJson(factoryDto), APPLICATION_JSON) +// .expect() +// .statusCode(200) + .when() + .post(SERVICE_PATH); + final FactoryDto result = getFromResponse(response, FactoryDto.class); + Map actualEnvs = result.getWorkspace().getEnvironments(); + assertEquals(actualEnvs, expectedEnvs); + } + private Factory createFactory() { return createNamedFactory(FACTORY_NAME); } diff --git a/wsmaster/che-core-api-workspace/pom.xml b/wsmaster/che-core-api-workspace/pom.xml index 045f133ee54..d04b4bc98d6 100644 --- a/wsmaster/che-core-api-workspace/pom.xml +++ b/wsmaster/che-core-api-workspace/pom.xml @@ -101,6 +101,10 @@ org.eclipse.che.core che-core-commons-lang + + org.everrest + everrest-core + org.everrest everrest-websockets @@ -179,11 +183,6 @@ everrest-assured test - - org.everrest - everrest-core - test - org.everrest everrest-test diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInEnvironmentUtil.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInEnvironmentUtil.java new file mode 100644 index 00000000000..f04240da751 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInEnvironmentUtil.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * 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.server.filters; + +import org.eclipse.che.api.workspace.shared.dto.EnvironmentDto; +import org.eclipse.che.api.workspace.shared.dto.ExtendedMachineDto; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto; + +import java.util.ArrayList; +import java.util.Map; + +/** + * Adds exec agent into each agents list in {@link WorkspaceConfigDto} where terminal agent is present. + * + * @author Alexander Garagatyi + */ +public class AddExecAgentInEnvironmentUtil { + public static void addExecAgent(WorkspaceConfigDto workspaceConfig) { + if (workspaceConfig != null) { + Map environments = workspaceConfig.getEnvironments(); + if (environments != null) { + for (EnvironmentDto environment : environments.values()) { + if (environment != null && environment.getMachines() != null) { + for (ExtendedMachineDto machine : environment.getMachines().values()) { + if (machine.getAgents() != null) { + if (machine.getAgents().contains("org.eclipse.che.terminal") && + !machine.getAgents().contains("org.eclipse.che.exec")) { + ArrayList updatedAgents = new ArrayList<>(machine.getAgents()); + updatedAgents.add("org.eclipse.che.exec"); + machine.setAgents(updatedAgents); + } + } + } + } + } + } + } + } +} diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInStackFilter.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInStackFilter.java new file mode 100644 index 00000000000..ef139b5e42e --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInStackFilter.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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.server.filters; + +import org.eclipse.che.api.core.ApiException; +import org.eclipse.che.api.workspace.shared.dto.stack.StackDto; +import org.eclipse.che.everrest.CheMethodInvokerFilter; +import org.everrest.core.Filter; +import org.everrest.core.resource.GenericResourceMethod; + +import javax.ws.rs.Path; + +/** + * Adds exec agent into each environment in stack where terminal agent is present. + * It is needed for backward compatibility of application behavior after separation of these agents. + * + * @author Alexander Garagatyi + */ +@Filter +@Path("/stack") +public class AddExecAgentInStackFilter extends CheMethodInvokerFilter { + @Override + protected void filter(GenericResourceMethod genericMethodResource, Object[] arguments) throws ApiException { + final String methodName = genericMethodResource.getMethod().getName(); + + switch (methodName) { + case "createStack": { + StackDto stack = (StackDto)arguments[0]; + AddExecAgentInEnvironmentUtil.addExecAgent(stack.getWorkspaceConfig()); + } + } + } +} diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInWorkspaceFilter.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInWorkspaceFilter.java new file mode 100644 index 00000000000..f11795afeb5 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInWorkspaceFilter.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * 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.server.filters; + +import org.eclipse.che.api.core.ApiException; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto; +import org.eclipse.che.everrest.CheMethodInvokerFilter; +import org.everrest.core.Filter; +import org.everrest.core.resource.GenericResourceMethod; + +import javax.ws.rs.Path; + +/** + * Adds exec agent into each environment in workspace config where terminal agent is present. + * It is needed for backward compatibility of application behavior after separation of these agents. + * + * @author Alexander Garagatyi + */ +@Filter +@Path("/workspace{path:(/.*)?}") +public class AddExecAgentInWorkspaceFilter extends CheMethodInvokerFilter { + @Override + protected void filter(GenericResourceMethod genericMethodResource, Object[] arguments) throws ApiException { + final String methodName = genericMethodResource.getMethod().getName(); + + switch (methodName) { + case "create": + case "startFromConfig": { + WorkspaceConfigDto workspaceConfig = (WorkspaceConfigDto)arguments[0]; + AddExecAgentInEnvironmentUtil.addExecAgent(workspaceConfig); + } + } + } +} diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/environment/server/MachineLinksInjector.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/environment/server/MachineLinksInjector.java index a54b5a21d4e..3b139e37ca6 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/environment/server/MachineLinksInjector.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/environment/server/MachineLinksInjector.java @@ -96,7 +96,7 @@ protected void injectExecAgentLink(MachineDto machine, ServiceContext serviceCon if (machine.getRuntime() != null) { final Collection servers = machine.getRuntime().getServers().values(); servers.stream() - .filter(server -> TERMINAL_REFERENCE.equals(server.getRef())) + .filter(server -> EXEC_AGENT_REFERENCE.equals(server.getRef())) .findAny() .ifPresent(terminal -> links.add(createLink("GET", diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceConfigJsonAdapter.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceConfigJsonAdapter.java index ab2fe4a91f0..a88d3409847 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceConfigJsonAdapter.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceConfigJsonAdapter.java @@ -151,6 +151,7 @@ private static JsonObject asEnvironment(JsonObject env, String envName) { // dev-machine agents final JsonArray agents = new JsonArray(); + agents.add(new JsonPrimitive("org.eclipse.che.exec")); agents.add(new JsonPrimitive("org.eclipse.che.terminal")); agents.add(new JsonPrimitive("org.eclipse.che.ws-agent")); agents.add(new JsonPrimitive("org.eclipse.che.ssh")); diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java index 7fea1c21ec8..8d3653f324a 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimes.java @@ -56,6 +56,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -424,7 +425,7 @@ public Instance startMachine(String workspaceId, // which means that original values won't affect the values in used further in this class MachineConfigImpl machineConfigCopy = new MachineConfigImpl(machineConfig); - List agents = Collections.singletonList("org.eclipse.che.terminal"); + List agents = Arrays.asList("org.eclipse.che.exec", "org.eclipse.che.terminal"); Instance instance = envEngine.startMachine(workspaceId, machineConfigCopy, agents); launchAgents(instance, agents); diff --git a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceServiceLinksInjector.java b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceServiceLinksInjector.java index 75724fb5579..2df44b62807 100644 --- a/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceServiceLinksInjector.java +++ b/wsmaster/che-core-api-workspace/src/main/java/org/eclipse/che/api/workspace/server/WorkspaceServiceLinksInjector.java @@ -231,9 +231,15 @@ protected void injectRuntimeLinks(WorkspaceDto workspace, URI ideUri, UriBuilder .build() .toString(), TERMINAL_REFERENCE)); + }); + + servers.stream() + .filter(server -> EXEC_AGENT_REFERENCE.equals(server.getRef())) + .findAny() + .ifPresent(exec -> { devMachine.getLinks() .add(createLink("GET", - UriBuilder.fromUri(terminal.getUrl()) + UriBuilder.fromUri(exec.getUrl()) .scheme("https".equals(ideUri.getScheme()) ? "wss" : "ws") .path("/connect") .build() diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInStackFilterTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInStackFilterTest.java new file mode 100644 index 00000000000..23076d9f1c0 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInStackFilterTest.java @@ -0,0 +1,238 @@ +/******************************************************************************* + * 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.server.filters; + +import com.google.common.collect.ImmutableMap; +import com.jayway.restassured.response.Response; + +import org.eclipse.che.api.core.ApiException; +import org.eclipse.che.api.core.rest.ApiExceptionMapper; +import org.eclipse.che.api.core.rest.CheJsonProvider; +import org.eclipse.che.api.workspace.server.stack.StackService; +import org.eclipse.che.api.workspace.shared.dto.EnvironmentDto; +import org.eclipse.che.api.workspace.shared.dto.ExtendedMachineDto; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto; +import org.eclipse.che.api.workspace.shared.dto.stack.StackDto; +import org.everrest.assured.EverrestJetty; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.util.HashSet; +import java.util.Map; + +import static com.jayway.restassured.RestAssured.given; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.che.dto.server.DtoFactory.cloneDto; +import static org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD; +import static org.everrest.assured.JettyHttpServer.SECURE_PATH; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +/** + * @author Alexander Garagatyi + */ +@Listeners(value = {EverrestJetty.class, MockitoTestNGListener.class}) +public class AddExecAgentInStackFilterTest { + + @SuppressWarnings("unused") + static final ApiExceptionMapper MAPPER = new ApiExceptionMapper(); + + @SuppressWarnings("unused") //is declared for deploying by everrest-assured + private CheJsonProvider jsonProvider = new CheJsonProvider(new HashSet<>()); + + @Mock + private StackService stackService; + @SuppressWarnings("unused") + @Spy + private AddExecAgentInStackFilter filter; + @Captor + private ArgumentCaptor stackCaptor; + + @BeforeMethod + public void setUp() throws Exception { + when(stackService.createStack(any(StackDto.class))).thenReturn(javax.ws.rs.core.Response.status(201).build()); + } + + @Test(dataProvider = "environmentsProvider") + public void shouldAddExecAgentIntoMachineWithTerminalAgent(Map inputEnv, + Map expectedEnv) throws ApiException { + StackDto inputStack = newDto(StackDto.class) + .withWorkspaceConfig(newDto(WorkspaceConfigDto.class).withEnvironments(inputEnv)); + + final Response response = given().auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .contentType(APPLICATION_JSON) + .body(inputStack) + .when() + .post(SECURE_PATH + "/stack"); + + assertEquals(response.getStatusCode(), 201); + + verify(stackService).createStack(stackCaptor.capture()); + Map actualEnv = stackCaptor.getValue().getWorkspaceConfig().getEnvironments(); + assertEquals(actualEnv, expectedEnv); + } + + @DataProvider(name = "environmentsProvider") + public static Object[][] environmentsProvider() { + EnvironmentDto environment = newDto(EnvironmentDto.class); + ExtendedMachineDto machine = newDto(ExtendedMachineDto.class); + return new Object[][] { + // no error if no envs + {emptyMap(), emptyMap()}, + + // no error if no machines in env + {singletonMap("e1", cloneDto(environment)), + singletonMap("e1", cloneDto(environment))}, + + // no error if no agents in machine + {singletonMap("e1", cloneDto(environment).withMachines(singletonMap("m1", cloneDto(machine)))), + singletonMap("e1", cloneDto(environment).withMachines(singletonMap("m1", cloneDto(machine))))}, + + // no error if agents list is empty + {singletonMap("e1", cloneDto(environment) + .withMachines(singletonMap("m1", cloneDto(machine).withAgents(emptyList())))), + singletonMap("e1", cloneDto(environment) + .withMachines(singletonMap("m1", cloneDto(machine).withAgents(emptyList()))))}, + + // don't add exec if existing agent is not terminal but start as terminal + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(singletonList("org.eclipse.che.terminal1"))))), + singletonMap("e1", cloneDto(environment).withMachines(singletonMap("m1", cloneDto(machine) + .withAgents(singletonList("org.eclipse.che.terminal1")))))}, + + // add exec agent if terminal is present + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(singletonList("org.eclipse.che.terminal"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.exec")))))}, + + // don't change agents if exec is present + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.exec", + "org.eclipse.che.terminal"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.exec", + "org.eclipse.che.terminal")))))}, + + // don't change agents if exec is present in the end of agents + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.exec"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.exec")))))}, + + // don't change agents if exec is present between other agents + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.exec", + "org.eclipse.che.ls.json"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.exec", + "org.eclipse.che.ls.json")))))}, + + // add exec in the end if terminal is present + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")))))}, + + // add exec into each machine with terminal + {singletonMap("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + )), + singletonMap("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ))}, + + // add exec into each machine with terminal in every env + {ImmutableMap.of("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ), + "e2", cloneDto(environment).withMachines( + ImmutableMap.of("m4", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json")), + "m5", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + )), + ImmutableMap.of("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ), + "e2", cloneDto(environment).withMachines( + ImmutableMap.of("m4", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m5", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ))}, + }; + } +} diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInWorkspaceFilterTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInWorkspaceFilterTest.java new file mode 100644 index 00000000000..aca31442141 --- /dev/null +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/agent/server/filters/AddExecAgentInWorkspaceFilterTest.java @@ -0,0 +1,254 @@ +/******************************************************************************* + * 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.server.filters; + +import com.google.common.collect.ImmutableMap; +import com.jayway.restassured.response.Response; + +import org.eclipse.che.api.core.rest.ApiExceptionMapper; +import org.eclipse.che.api.core.rest.CheJsonProvider; +import org.eclipse.che.api.workspace.server.WorkspaceService; +import org.eclipse.che.api.workspace.shared.dto.EnvironmentDto; +import org.eclipse.che.api.workspace.shared.dto.ExtendedMachineDto; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceDto; +import org.everrest.assured.EverrestJetty; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.util.HashSet; +import java.util.Map; + +import static com.jayway.restassured.RestAssured.given; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.eclipse.che.dto.server.DtoFactory.cloneDto; +import static org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME; +import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD; +import static org.everrest.assured.JettyHttpServer.SECURE_PATH; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyListOf; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +/** + * @author Alexander Garagatyi + */ +@Listeners(value = {EverrestJetty.class, MockitoTestNGListener.class}) +public class AddExecAgentInWorkspaceFilterTest { + @SuppressWarnings("unused") //is declared for deploying by everrest-assured + private static final ApiExceptionMapper MAPPER = new ApiExceptionMapper(); + + @SuppressWarnings("unused") //is declared for deploying by everrest-assured + private CheJsonProvider jsonProvider = new CheJsonProvider(new HashSet<>()); + + @Mock + private WorkspaceService workspaceService; + @SuppressWarnings("unused") + @Spy + private AddExecAgentInWorkspaceFilter filter; + @Captor + private ArgumentCaptor workspaceConfigCaptor; + + @BeforeMethod + public void setUp() throws Exception { + when(workspaceService.create(any(WorkspaceConfigDto.class), anyListOf(String.class), anyBoolean(), anyString())) + .thenReturn(javax.ws.rs.core.Response.status(201).build()); + when(workspaceService.startFromConfig(any(WorkspaceConfigDto.class), anyBoolean(), anyString())) + .thenReturn(newDto(WorkspaceDto.class)); + } + + @Test(dataProvider = "environmentsProvider") + public void shouldAddExecAgentIfNeededOnCreateWS(Map inputEnv, + Map expectedEnv) throws Exception { + final Response response = given().auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .contentType("application/json") + .body(newDto(WorkspaceConfigDto.class).withEnvironments(inputEnv)) + .when() + .post(SECURE_PATH + "/workspace"); + + assertEquals(response.getStatusCode(), 201); + verify(workspaceService).create(workspaceConfigCaptor.capture(), any(), anyBoolean(), anyString()); + Map actualEnv = workspaceConfigCaptor.getValue().getEnvironments(); + assertEquals(actualEnv, expectedEnv); + } + + @Test(dataProvider = "environmentsProvider") + public void shouldAddExecAgentIfNeededOnStartWSFromConfig(Map inputEnv, + Map expectedEnv) + throws Exception { + final Response response = given().auth() + .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .contentType("application/json") + .body(newDto(WorkspaceConfigDto.class).withEnvironments(inputEnv)) + .when() + .post(SECURE_PATH + "/workspace/runtime"); + + assertEquals(response.getStatusCode(), 200); + verify(workspaceService).startFromConfig(workspaceConfigCaptor.capture(), anyBoolean(), anyString()); + Map actualEnv = workspaceConfigCaptor.getValue().getEnvironments(); + assertEquals(actualEnv, expectedEnv); + } + + @DataProvider(name = "environmentsProvider") + public static Object[][] environmentsProvider() { + EnvironmentDto environment = newDto(EnvironmentDto.class); + ExtendedMachineDto machine = newDto(ExtendedMachineDto.class); + return new Object[][] { + // no error if no envs + {emptyMap(), emptyMap()}, + + // no error if no machines in env + {singletonMap("e1", cloneDto(environment)), + singletonMap("e1", cloneDto(environment))}, + + // no error if no agents in machine + {singletonMap("e1", cloneDto(environment).withMachines(singletonMap("m1", cloneDto(machine)))), + singletonMap("e1", cloneDto(environment).withMachines(singletonMap("m1", cloneDto(machine))))}, + + // no error if agents list is empty + {singletonMap("e1", cloneDto(environment) + .withMachines(singletonMap("m1", cloneDto(machine).withAgents(emptyList())))), + singletonMap("e1", cloneDto(environment) + .withMachines(singletonMap("m1", cloneDto(machine).withAgents(emptyList()))))}, + + // don't add exec if existing agent is not terminal but start as terminal + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(singletonList("org.eclipse.che.terminal1"))))), + singletonMap("e1", cloneDto(environment).withMachines(singletonMap("m1", cloneDto(machine) + .withAgents(singletonList("org.eclipse.che.terminal1")))))}, + + // add exec agent if terminal is present + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(singletonList("org.eclipse.che.terminal"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.exec")))))}, + + // don't change agents if exec is present + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.exec", + "org.eclipse.che.terminal"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.exec", + "org.eclipse.che.terminal")))))}, + + // don't change agents if exec is present in the end of agents + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.exec"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.exec")))))}, + + // don't change agents if exec is present between other agents + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.exec", + "org.eclipse.che.ls.json"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.exec", + "org.eclipse.che.ls.json")))))}, + + // add exec in the end if terminal is present + {singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))))), + singletonMap("e1", cloneDto(environment).withMachines( + singletonMap("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")))))}, + + // add exec into each machine with terminal + {singletonMap("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + )), + singletonMap("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ))}, + + // add exec into each machine with terminal in every env + {ImmutableMap.of("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ), + "e2", cloneDto(environment).withMachines( + ImmutableMap.of("m4", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json")), + "m5", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + )), + ImmutableMap.of("e1", cloneDto(environment).withMachines( + ImmutableMap.of("m1", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m2", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.terminal", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m3", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ), + "e2", cloneDto(environment).withMachines( + ImmutableMap.of("m4", cloneDto(machine).withAgents(asList("org.eclipse.che.terminal", + "org.eclipse.che.ls.php", + "org.eclipse.che.ls.json", + "org.eclipse.che.exec")), + "m5", cloneDto(machine).withAgents(asList("org.eclipse.che.ls.php", + "org.eclipse.che.ls.json"))) + ))}, + }; + } +} diff --git a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java index 56c61b75137..5764ac0b480 100644 --- a/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java +++ b/wsmaster/che-core-api-workspace/src/test/java/org/eclipse/che/api/workspace/server/WorkspaceRuntimesTest.java @@ -998,11 +998,13 @@ private Instance newMachine(String workspaceId, String envName, String name, boo private WorkspaceImpl newWorkspace(String workspaceId, String envName) { EnvironmentImpl environment = new EnvironmentImpl(); Map machines = environment.getMachines(); - machines.put("dev", new ExtendedMachineImpl(Arrays.asList("org.eclipse.che.terminal", + machines.put("dev", new ExtendedMachineImpl(Arrays.asList("org.eclipse.che.exec", + "org.eclipse.che.terminal", "org.eclipse.che.ws-agent"), Collections.emptyMap(), Collections.emptyMap())); - machines.put("db", new ExtendedMachineImpl(singletonList("org.eclipse.che.terminal"), + machines.put("db", new ExtendedMachineImpl(Arrays.asList("org.eclipse.che.exec", + "org.eclipse.che.terminal"), Collections.emptyMap(), Collections.emptyMap())); return WorkspaceImpl.builder() diff --git a/wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.6.0/1__add_exec_agent_where_terminal_agent_is_present.sql b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.6.0/1__add_exec_agent_where_terminal_agent_is_present.sql new file mode 100644 index 00000000000..5cb2f5f69e7 --- /dev/null +++ b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.6.0/1__add_exec_agent_where_terminal_agent_is_present.sql @@ -0,0 +1,16 @@ +-- +-- 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 +-- + +INSERT INTO externalmachine_agents +(externalmachine_id, agents) + SELECT externalmachine_id, 'org.eclipse.che.exec' + FROM externalmachine_agents + WHERE agents = 'org.eclipse.che.terminal'