/******************************************************************************* * Copyright (c) 2015, 2016, 2017 Red Hat Inc. and others. * 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: * Red Hat - Initial Contribution *******************************************************************************/ package org.eclipse.linuxtools.docker.ui.launch; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.linuxtools.docker.core.DockerConnectionManager; import org.eclipse.linuxtools.docker.core.DockerException; import org.eclipse.linuxtools.docker.core.EnumDockerLoggingStatus; import org.eclipse.linuxtools.docker.core.IDockerConnection; import org.eclipse.linuxtools.docker.core.IDockerContainerConfig; import org.eclipse.linuxtools.docker.core.IDockerContainerExit; import org.eclipse.linuxtools.docker.core.IDockerContainerInfo; import org.eclipse.linuxtools.docker.core.IDockerHostConfig; import org.eclipse.linuxtools.docker.core.IDockerImage; import org.eclipse.linuxtools.docker.core.IDockerImageInfo; import org.eclipse.linuxtools.docker.core.IDockerPortBinding; import org.eclipse.linuxtools.docker.ui.Activator; import org.eclipse.linuxtools.internal.docker.core.DockerConnection; import org.eclipse.linuxtools.internal.docker.core.DockerContainerConfig; import org.eclipse.linuxtools.internal.docker.core.DockerHostConfig; import org.eclipse.linuxtools.internal.docker.core.DockerPortBinding; import org.eclipse.linuxtools.internal.docker.ui.consoles.ConsoleOutputStream; import org.eclipse.linuxtools.internal.docker.ui.consoles.RunConsole; import org.eclipse.linuxtools.internal.docker.ui.launch.ContainerCommandProcess; import org.eclipse.linuxtools.internal.docker.ui.views.DVMessages; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.PlatformUI; public class ContainerLauncher { private static final String ERROR_CREATING_CONTAINER = "ContainerCreateError.msg"; //$NON-NLS-1$ private static final String ERROR_LAUNCHING_CONTAINER = "ContainerLaunchError.msg"; //$NON-NLS-1$ private static final String ERROR_NO_CONNECTIONS = "ContainerNoConnections.msg"; //$NON-NLS-1$ private static final String ERROR_NO_CONNECTION_WITH_URI = "ContainerNoConnectionWithURI.msg"; //$NON-NLS-1$ private static final String DIRFILE_NAME = "copiedVolumes"; //$NON-NLS-1$ private static RunConsole console; private static Map<IProject, ID> fidMap = new HashMap<>(); private static Object lockObject = new Object(); private static Map<String, Map<String, Set<String>>> copiedVolumesMap = null; private class CopyVolumesJob extends Job { private static final String COPY_VOLUMES_JOB_TITLE = "ContainerLaunch.copyVolumesJob.title"; //$NON-NLS-1$ private static final String COPY_VOLUMES_DESC = "ContainerLaunch.copyVolumesJob.desc"; //$NON-NLS-1$ private static final String COPY_VOLUMES_TASK = "ContainerLaunch.copyVolumesJob.task"; //$NON-NLS-1$ private static final String ERROR_COPYING_VOLUME = "ContainerLaunch.copyVolumesJob.error"; //$NON-NLS-1$ private final Set<String> volumes; private final IDockerConnection connection; private final String containerId; public CopyVolumesJob(Set<String> volumes, IDockerConnection connection, String containerId) { super(Messages.getString(COPY_VOLUMES_JOB_TITLE)); this.volumes = volumes; this.connection = connection; this.containerId = containerId; } @Override protected IStatus run(IProgressMonitor monitor) { monitor.beginTask( Messages.getFormattedString(COPY_VOLUMES_DESC, containerId), volumes.size()); Iterator<String> iterator = volumes.iterator(); IStatus status = Status.OK_STATUS; // for each remote volume, copy from host to Container volume while (iterator.hasNext()) { if (monitor.isCanceled()) { monitor.done(); return Status.CANCEL_STATUS; } String directory = iterator.next(); if (!directory.endsWith("/")) { //$NON-NLS-1$ directory = directory + "/"; //$NON-NLS-1$ } monitor.setTaskName(Messages .getFormattedString(COPY_VOLUMES_TASK, directory)); try { ((DockerConnection) connection).copyToContainer(directory, containerId, directory); monitor.worked(1); } catch (DockerException | InterruptedException | IOException e) { monitor.done(); final String dir = directory; Display.getDefault().syncExec(() -> MessageDialog.openError( PlatformUI.getWorkbench().getActiveWorkbenchWindow() .getShell(), Messages.getFormattedString(ERROR_COPYING_VOLUME, new String[] { dir, containerId }), e.getMessage())); status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage()); } finally { monitor.done(); } } return status; } } private class CopyVolumesFromImageJob extends Job { private static final String COPY_VOLUMES_FROM_JOB_TITLE = "ContainerLaunch.copyVolumesFromJob.title"; //$NON-NLS-1$ private static final String COPY_VOLUMES_FROM_DESC = "ContainerLaunch.copyVolumesFromJob.desc"; //$NON-NLS-1$ private static final String COPY_VOLUMES_FROM_TASK = "ContainerLaunch.copyVolumesFromJob.task"; //$NON-NLS-1$ private final List<String> volumes; private final IDockerConnection connection; private final String image; private final IPath target; private Set<String> dirList; public CopyVolumesFromImageJob( IDockerConnection connection, String image, List<String> volumes, IPath target) { super(Messages.getString(COPY_VOLUMES_FROM_JOB_TITLE)); this.volumes = volumes; this.connection = connection; this.image = image; this.target = target; Map<String, Set<String>> dirMap = null; synchronized (lockObject) { String uri = connection.getUri(); dirMap = copiedVolumesMap.get(uri); if (dirMap == null) { dirMap = new HashMap<>(); copiedVolumesMap.put(uri, dirMap); } dirList = dirMap.get(image); if (dirList == null) { dirList = new HashSet<>(); dirMap.put(image, dirList); } } } @Override protected IStatus run(final IProgressMonitor monitor) { monitor.beginTask( Messages.getFormattedString(COPY_VOLUMES_FROM_DESC, image), volumes.size()); String containerId = null; try { IDockerImage dockerImage = ((DockerConnection) connection) .getImageByTag(image); // if there is a .image_id file, check the image id to ensure // the user hasn't loaded a new version which may have // different header files installed. IPath imageFilePath = target.append(".image_id"); //$NON-NLS-1$ File imageFile = imageFilePath.toFile(); boolean needImageIdFile = !imageFile.exists(); if (!needImageIdFile) { try (FileReader reader = new FileReader(imageFile); BufferedReader bufferReader = new BufferedReader( reader);) { String imageId = bufferReader.readLine(); if (!dockerImage.id().equals(imageId)) { // if image id has changed...all bets are off // and we must reload all directories dirList.clear(); needImageIdFile = true; } } catch (IOException e) { // ignore } } if (needImageIdFile) { try (FileWriter writer = new FileWriter(imageFile); BufferedWriter bufferedWriter = new BufferedWriter( writer);) { bufferedWriter.write(dockerImage.id()); bufferedWriter.newLine(); } catch (IOException e) { // ignore } } DockerContainerConfig.Builder builder = new DockerContainerConfig.Builder() .cmd("/bin/sh").image(image); //$NON-NLS-1$ IDockerContainerConfig config = builder.build(); DockerHostConfig.Builder hostBuilder = new DockerHostConfig.Builder(); IDockerHostConfig hostConfig = hostBuilder.build(); containerId = ((DockerConnection) connection) .createContainer(config, hostConfig, null); for (String volume : volumes) { if (monitor.isCanceled()) { monitor.done(); return Status.CANCEL_STATUS; } // don't bother copying files from project if (volume.contains("${ProjName}")) { //$NON-NLS-1$ monitor.worked(1); continue; } // if we have already copied the directory either directly // or as part of a parent directory copy, then skip to next // volume. for (String path : dirList) { if (volume.equals(path) || (volume.startsWith(path) && volume.charAt( path.length()) == File.separatorChar)) { monitor.worked(1); continue; } } try { monitor.setTaskName(Messages.getFormattedString( COPY_VOLUMES_FROM_TASK, volume)); monitor.worked(1); InputStream in = ((DockerConnection) connection) .copyContainer(containerId, volume); synchronized (lockObject) { dirList.add(volume); } /* * The input stream from copyContainer might be * incomplete or non-blocking so we should wrap it in a * stream that is guaranteed to block until data is * available. */ TarArchiveInputStream k = new TarArchiveInputStream( new BlockingInputStream(in)); TarArchiveEntry te = null; target.toFile().mkdirs(); IPath currDir = target.append(volume) .removeLastSegments(1); currDir.toFile().mkdirs(); while ((te = k.getNextTarEntry()) != null) { long size = te.getSize(); IPath path = currDir; path = path.append(te.getName()); File f = new File(path.toOSString()); if (te.isDirectory()) { f.mkdir(); continue; } else { f.createNewFile(); } FileOutputStream os = new FileOutputStream(f); int bufferSize = ((int) size > 4096 ? 4096 : (int) size); byte[] barray = new byte[bufferSize]; int result = -1; while ((result = k.read(barray, 0, bufferSize)) > -1) { if (monitor.isCanceled()) { monitor.done(); k.close(); os.close(); return Status.CANCEL_STATUS; } os.write(barray, 0, result); } os.close(); } k.close(); } catch (final DockerException e) { // ignore } } } catch (InterruptedException e) { // do nothing } catch (IOException e) { Activator.log(e); } catch (DockerException e1) { Activator.log(e1); } finally { if (containerId != null) { try { ((DockerConnection) connection) .removeContainer(containerId); } catch (DockerException | InterruptedException e) { // ignore } } monitor.done(); } return Status.OK_STATUS; } } /** * A blocking input stream that waits until data is available. */ private class BlockingInputStream extends InputStream { private InputStream in; public BlockingInputStream(InputStream in) { this.in = in; } @Override public int read() throws IOException { return in.read(); } } @Override protected void finalize() throws Throwable { synchronized (lockObject) { if (copiedVolumesMap != null) { IPath pluginPath = Platform.getStateLocation( Platform.getBundle(Activator.PLUGIN_ID)); IPath path = pluginPath.append(DIRFILE_NAME); File dirFile = path.toFile(); FileOutputStream f = new FileOutputStream(dirFile); try (ObjectOutputStream oos = new ObjectOutputStream(f)) { oos.writeObject(copiedVolumesMap); } } } super.finalize(); } public ContainerLauncher() { initialize(); } @SuppressWarnings("unchecked") private void initialize() { synchronized (lockObject) { if (copiedVolumesMap == null) { IPath pluginPath = Platform.getStateLocation( Platform.getBundle(Activator.PLUGIN_ID)); IPath path = pluginPath.append(DIRFILE_NAME); File dirFile = path.toFile(); if (dirFile.exists()) { try (FileInputStream f = new FileInputStream(dirFile)) { try (ObjectInputStream ois = new ObjectInputStream(f)) { copiedVolumesMap = (Map<String, Map<String, Set<String>>>) ois .readObject(); } catch (ClassNotFoundException | FileNotFoundException e) { // should never happen so print stack trace e.printStackTrace(); } } catch (IOException e) { // will handle this below } } } if (copiedVolumesMap == null) { copiedVolumesMap = new HashMap<>(); } } } /** * Perform a launch of a command in a container and output stdout/stderr to * console. * * @param id * - id of caller to use to distinguish console owner * @param listener * - optional listener of the run console * @param connectionUri * - the specified connection to use * @param image * - the image to use * @param command * - command to run * @param commandDir * - directory command requires or null * @param workingDir * - working directory or null * @param additionalDirs * - additional directories to mount or null * @param origEnv * - original environment if we are appending to our existing * environment * @param envMap * - map of environment variable settings * @param ports * - ports to expose * @param keep * - keep container after running * @param stdinSupport * - true if stdin support is required, false otherwise */ public void launch(String id, IContainerLaunchListener listener, final String connectionUri, String image, String command, String commandDir, String workingDir, List<String> additionalDirs, Map<String, String> origEnv, Map<String, String> envMap, List<String> ports, boolean keep, boolean stdinSupport) { launch(id, listener, connectionUri, image, command, commandDir, workingDir, additionalDirs, origEnv, envMap, ports, keep, stdinSupport, false); } /** * Perform a launch of a command in a container and output stdout/stderr to * console. * * @param id * - id of caller to use to distinguish console owner * @param listener * - optional listener of the run console * @param connectionUri * - the specified connection to use * @param image * - the image to use * @param command * - command to run * @param commandDir * - directory command requires or null * @param workingDir * - working directory or null * @param additionalDirs * - additional directories to mount or null * @param origEnv * - original environment if we are appending to our existing * environment * @param envMap * - map of environment variable settings * @param ports * - ports to expose * @param keep * - keep container after running * @param stdinSupport * - true if stdin support is required, false otherwise * @param privilegedMode * - true if privileged mode is required, false otherwise * @since 2.1 */ public void launch(String id, IContainerLaunchListener listener, final String connectionUri, String image, String command, String commandDir, String workingDir, List<String> additionalDirs, Map<String, String> origEnv, Map<String, String> envMap, List<String> ports, boolean keep, boolean stdinSupport, boolean privilegedMode) { launch(id, listener, connectionUri, image, command, commandDir, workingDir, additionalDirs, origEnv, envMap, ports, keep, stdinSupport, privilegedMode, null); } /** * Perform a launch of a command in a container and output stdout/stderr to * console. * * @param id * - id of caller to use to distinguish console owner * @param listener * - optional listener of the run console * @param connectionUri * - the specified connection to use * @param image * - the image to use * @param command * - command to run * @param commandDir * - directory command requires or null * @param workingDir * - working directory or null * @param additionalDirs * - additional directories to mount or null * @param origEnv * - original environment if we are appending to our existing * environment * @param envMap * - map of environment variable settings * @param ports * - ports to expose * @param keep * - keep container after running * @param stdinSupport * - true if stdin support is required, false otherwise * @param privilegedMode * - true if privileged mode is required, false otherwise * @param labels * - Map of labels for the container * @since 2.2 */ public void launch(String id, IContainerLaunchListener listener, final String connectionUri, String image, String command, String commandDir, String workingDir, List<String> additionalDirs, Map<String, String> origEnv, Map<String, String> envMap, List<String> ports, boolean keep, boolean stdinSupport, boolean privilegedMode, Map<String, String> labels) { launch(id, listener, connectionUri, image, command, commandDir, workingDir, additionalDirs, origEnv, envMap, ports, keep, stdinSupport, privilegedMode, labels, null); } /** * Perform a launch of a command in a container and output stdout/stderr to * console. * * @param id * - id of caller to use to distinguish console owner * @param listener * - optional listener of the run console * @param connectionUri * - the specified connection to use * @param image * - the image to use * @param command * - command to run * @param commandDir * - directory command requires or null * @param workingDir * - working directory or null * @param additionalDirs * - additional directories to mount or null * @param origEnv * - original environment if we are appending to our existing * environment * @param envMap * - map of environment variable settings * @param ports * - ports to expose * @param keep * - keep container after running * @param stdinSupport * - true if stdin support is required, false otherwise * @param privilegedMode * - true if privileged mode is required, false otherwise * @param labels * - Map of labels for the container * @param seccomp * - seccomp profile * @since 3.0 */ public void launch(String id, IContainerLaunchListener listener, final String connectionUri, String image, String command, String commandDir, String workingDir, List<String> additionalDirs, Map<String, String> origEnv, Map<String, String> envMap, List<String> ports, boolean keep, boolean stdinSupport, boolean privilegedMode, Map<String, String> labels, String seccomp) { final String LAUNCH_TITLE = "ContainerLaunch.title"; //$NON-NLS-1$ final String LAUNCH_EXITED_TITLE = "ContainerLaunchExited.title"; //$NON-NLS-1$ final List<String> env = new ArrayList<>(); env.addAll(toList(origEnv)); env.addAll(toList(envMap)); final List<String> cmdList = getCmdList(command); final Set<String> exposedPorts = new HashSet<>(); final Map<String, List<IDockerPortBinding>> portBindingsMap = new HashMap<>(); if (ports != null) { for (String port : ports) { port = port.trim(); if (port.length() > 0) { String[] segments = port.split(":"); //$NON-NLS-1$ if (segments.length == 1) { // containerPort exposedPorts.add(segments[0]); portBindingsMap .put(segments[0], Arrays.asList((IDockerPortBinding) new DockerPortBinding( "", ""))); //$NON-NLS-1$ //$NON-NLS-2$ } else if (segments.length == 2) { // hostPort:containerPort exposedPorts.add(segments[1]); portBindingsMap .put(segments[1], Arrays.asList((IDockerPortBinding) new DockerPortBinding( "", segments[0]))); //$NON-NLS-1$ //$NON-NLS-2$ } else if (segments.length == 3) { // either // ip:hostPort:containerPort // or ip::containerPort exposedPorts.add(segments[1]); if (segments[1].isEmpty()) { portBindingsMap .put(segments[2], Arrays.asList((IDockerPortBinding) new DockerPortBinding( "", segments[0]))); //$NON-NLS-1$ //$NON-NLS-2$ } else { portBindingsMap .put(segments[2], Arrays.asList((IDockerPortBinding) new DockerPortBinding( segments[0], segments[1]))); //$NON-NLS-1$ //$NON-NLS-2$ } } } } } // Note we only pass volumes to the config if we have a // remote daemon. Local mounted volumes are passed // via the HostConfig binds setting DockerContainerConfig.Builder builder = new DockerContainerConfig.Builder() .openStdin(stdinSupport) .cmd(cmdList) .image(image) .workingDir(workingDir); // add any exposed ports as needed if (exposedPorts.size() > 0) builder = builder.exposedPorts(exposedPorts); // add any labels if specified if (labels != null) builder = builder.labels(labels); if (!DockerConnectionManager.getInstance().hasConnections()) { Display.getDefault() .syncExec(() -> MessageDialog.openError( PlatformUI.getWorkbench().getActiveWorkbenchWindow() .getShell(), DVMessages.getString(ERROR_LAUNCHING_CONTAINER), DVMessages.getString(ERROR_NO_CONNECTIONS))); return; } // Try and use the specified connection that was used before, // otherwise, open an error final IDockerConnection connection = DockerConnectionManager .getInstance().getConnectionByUri(connectionUri); if (connection == null) { Display.getDefault() .syncExec(() -> MessageDialog.openError( PlatformUI.getWorkbench().getActiveWorkbenchWindow() .getShell(), DVMessages.getString(ERROR_LAUNCHING_CONTAINER), DVMessages.getFormattedString( ERROR_NO_CONNECTION_WITH_URI, connectionUri))); return; } DockerHostConfig.Builder hostBuilder = new DockerHostConfig.Builder() .privileged(privilegedMode); // specify seccomp profile if caller has provided one - needed to use // ptrace with gdbserver if (seccomp != null) { hostBuilder.securityOpt(seccomp); } final Set<String> remoteVolumes = new TreeSet<>(); if (!((DockerConnection) connection).isLocal()) { // if using remote daemon, we have to // handle volume mounting differently. // Instead we mount empty volumes and copy // the host data over before starting. if (additionalDirs != null) { for (String dir : additionalDirs) { remoteVolumes.add(dir); // $NON-NLS-1$ } } if (workingDir != null) remoteVolumes.add(workingDir); // $NON-NLS-1$ if (commandDir != null) remoteVolumes.add(commandDir); // $NON-NLS-1$ builder = builder.volumes(remoteVolumes); } else { // Running daemon on local host. // Add mounts for any directories we need to run the executable. // When we add mount points, we need entries of the form: // hostname:mountname:Z. // In our case, we want all directories mounted as-is so the // executable will run as the user expects. final List<String> volumes = new ArrayList<>(); if (additionalDirs != null) { for (String dir : additionalDirs) { volumes.add(dir + ":" + dir + ":Z"); //$NON-NLS-1$ //$NON-NLS-2$ } } if (workingDir != null) { volumes.add(workingDir + ":" + workingDir + ":Z"); //$NON-NLS-1$ //$NON-NLS-2$ } if (commandDir != null) { volumes.add(commandDir + ":" + commandDir + ":Z"); //$NON-NLS-1$ //$NON-NLS-2$ } hostBuilder = hostBuilder.binds(volumes); } final DockerContainerConfig config = builder.build(); // add any port bindings if specified if (portBindingsMap.size() > 0) hostBuilder = hostBuilder.portBindings(portBindingsMap); final IDockerHostConfig hostConfig = hostBuilder.build(); final String imageName = image; final boolean keepContainer = keep; final String consoleId = id; final IContainerLaunchListener containerListener = listener; Thread t = new Thread(() -> { // create the container String containerId = null; try { containerId = ((DockerConnection) connection) .createContainer(config, hostConfig, null); if (!((DockerConnection) connection).isLocal()) { // if daemon is remote, we need to copy // data over from the host. if (!remoteVolumes.isEmpty()) { CopyVolumesJob job = new CopyVolumesJob(remoteVolumes, connection, containerId); job.schedule(); job.join(); if (job.getResult() != Status.OK_STATUS) return; } } OutputStream stream = null; RunConsole oldConsole = getConsole(); final RunConsole rc = RunConsole.findConsole(containerId, consoleId); setConsole(rc); rc.clearConsole(); if (oldConsole != null) RunConsole.removeConsole(oldConsole); Display.getDefault() .syncExec(() -> rc.setTitle(Messages.getFormattedString( LAUNCH_TITLE, new String[] { cmdList.get(0), imageName }))); // if (!rc.isAttached()) { rc.attachToConsole(connection, containerId); // } if (rc != null) { stream = rc.getOutputStream(); if (containerListener != null) { ((ConsoleOutputStream) stream) .addConsoleListener(containerListener); } } // Create a unique logging thread id which has container id // and console id String loggingId = containerId + "." + consoleId; ((DockerConnection) connection).startContainer(containerId, loggingId, stream); if (rc != null) rc.showConsole(); if (containerListener != null) { IDockerContainerInfo info = ((DockerConnection) connection) .getContainerInfo(containerId); containerListener.containerInfo(info); } // Wait for the container to finish final IDockerContainerExit status = ((DockerConnection) connection) .waitForContainer(containerId); Display.getDefault().syncExec(() -> { rc.setTitle( Messages.getFormattedString(LAUNCH_EXITED_TITLE, new String[] { status.statusCode().toString(), cmdList.get(0), imageName })); rc.showConsole(); }); // Let any container listener know that the container is // finished if (containerListener != null) containerListener.done(); if (!keepContainer) { // Drain the logging thread before we remove the // container (we need to use the logging id) ((DockerConnection) connection) .stopLoggingThread(loggingId); while (((DockerConnection) connection).loggingStatus( loggingId) == EnumDockerLoggingStatus.LOGGING_ACTIVE) { Thread.sleep(1000); } // Look for any Display Log console that the user may // have opened which would be // separate and make sure it is removed as well RunConsole rc2 = RunConsole .findConsole(((DockerConnection) connection) .getContainer(containerId)); if (rc2 != null) RunConsole.removeConsole(rc2); ((DockerConnection) connection) .removeContainer(containerId); } } catch (final DockerException e2) { // error in creation, try and remove Container if possible if (!keepContainer && containerId != null) { try { ((DockerConnection) connection) .removeContainer(containerId); } catch (DockerException | InterruptedException e1) { // ignore exception } } Display.getDefault().syncExec(() -> MessageDialog.openError( PlatformUI.getWorkbench().getActiveWorkbenchWindow() .getShell(), DVMessages.getFormattedString(ERROR_CREATING_CONTAINER, imageName), e2.getMessage())); } catch (InterruptedException e3) { // for now // do nothing } ((DockerConnection) connection).getContainers(true); }); t.start(); } private class ID { private Integer uid; private Integer gid; public ID(Integer uid, Integer gid) { this.uid = uid; this.gid = gid; } public Integer getuid() { return uid; } public Integer getgid() { return gid; } } /** * Fetch directories from Container and place them in a specified location. * * @param connectionUri * - uri of connection to use * @param imageName * - name of image to use * @param containerDirs * - list of directories to copy * @param hostDir * - host directory to copy directories to * @return 0 if successful, -1 if failure occurred * * @since 3.0 */ public int fetchContainerDirs(String connectionUri, String imageName, List<String> containerDirs, IPath hostDir) { // Try and use the specified connection that was used before, // otherwise, open an error final IDockerConnection connection = DockerConnectionManager .getInstance().getConnectionByUri(connectionUri); if (connection == null) { Display.getDefault() .syncExec(() -> MessageDialog.openError( PlatformUI.getWorkbench().getActiveWorkbenchWindow() .getShell(), DVMessages.getString(ERROR_LAUNCHING_CONTAINER), DVMessages.getFormattedString( ERROR_NO_CONNECTION_WITH_URI, connectionUri))); return -1; } CopyVolumesFromImageJob job = new CopyVolumesFromImageJob(connection, imageName, containerDirs, hostDir); job.schedule(); return 0; } /** * Create a Process to run an arbitrary command in a Container with uid of * caller so any files created are accessible to user. * * @param connectionName * - uri of connection to use * @param imageName * - name of image to use * @param project * - Eclipse project * @param errMsgHolder * - holder for any error messages * @param command * - command to run * @param commandDir * - directory path to command * @param workingDir * - where to run command * @param additionalDirs * - additional directories to mount * @param origEnv * - original environment if we are appending to existing * @param envMap * - new environment * @param supportStdin * - support using stdin * @param privilegedMode * - run in privileged mode * @param labels * - labels to apply to Container * @param keepContainer * - boolean whether to keep Container when done * @return Process that can be used to check for completion and for routing * stdout/stderr * * @since 3.0 */ public Process runCommand(String connectionName, String imageName, IProject project, IErrorMessageHolder errMsgHolder, String command, String commandDir, String workingDir, List<String> additionalDirs, Map<String, String> origEnv, Properties envMap, boolean supportStdin, boolean privilegedMode, HashMap<String, String> labels, boolean keepContainer) { Integer uid = null; Integer gid = null; // For Unix, make sure that the user id is passed with the run // so any output files are accessible by this end-user String os = System.getProperty("os.name"); //$NON-NLS-1$ if (os.indexOf("nux") > 0) { //$NON-NLS-1$ // first try and see if we have already run a command on this // project ID ugid = fidMap.get(project); if (ugid == null) { try { uid = (Integer) Files.getAttribute( project.getLocation().toFile().toPath(), "unix:uid"); //$NON-NLS-1$ gid = (Integer) Files.getAttribute( project.getLocation().toFile().toPath(), "unix:gid"); //$NON-NLS-1$ ugid = new ID(uid, gid); // store the uid for possible later usage fidMap.put(project, ugid); } catch (IOException e) { // do nothing...leave as null } // $NON-NLS-1$ } else { uid = ugid.getuid(); gid = ugid.getgid(); } } final List<String> env = new ArrayList<>(); env.addAll(toList(origEnv)); env.addAll(toList(envMap)); final List<String> cmdList = getCmdList(command); final Map<String, List<IDockerPortBinding>> portBindingsMap = new HashMap<>(); IDockerConnection[] connections = DockerConnectionManager .getInstance().getConnections(); if (connections == null || connections.length == 0) { errMsgHolder.setErrorMessage( Messages.getString("ContainerLaunch.noConnections.error")); //$NON-NLS-1$ return null; } IDockerConnection connection = null; for (IDockerConnection c : connections) { if (c.getUri().equals(connectionName)) { connection = c; break; } } if (connection == null) { errMsgHolder.setErrorMessage(Messages.getFormattedString( "ContainerLaunch.connectionNotFound.error", //$NON-NLS-1$ connectionName)); return null; } List<IDockerImage> images = connection.getImages(); if (images.isEmpty()) { errMsgHolder.setErrorMessage( Messages.getString("ContainerLaunch.noImages.error")); //$NON-NLS-1$ return null; } IDockerImageInfo info = connection.getImageInfo(imageName); if (info == null) { errMsgHolder.setErrorMessage(Messages.getFormattedString( "ContainerLaunch.imageNotFound.error", imageName)); //$NON-NLS-1$ return null; } DockerContainerConfig.Builder builder = new DockerContainerConfig.Builder() .openStdin(supportStdin).cmd(cmdList).image(imageName) .workingDir(workingDir); // switch to user id for Linux so output is accessible if (uid != null) { builder = builder.user(uid.toString()); } // TODO: add group id here when supported by DockerHostConfig.Builder // add any labels if specified if (labels != null) builder = builder.labels(labels); DockerHostConfig.Builder hostBuilder = new DockerHostConfig.Builder() .privileged(privilegedMode); // Note we only pass volumes to the config if we have a // remote daemon. Local mounted volumes are passed // via the HostConfig binds setting final Set<String> remoteVolumes = new TreeSet<>(); if (!((DockerConnection) connection).isLocal()) { // if using remote daemon, we have to // handle volume mounting differently. // Instead we mount empty volumes and copy // the host data over before starting. if (additionalDirs != null) { for (String dir : additionalDirs) { IPath p = new Path(dir).removeTrailingSeparator(); remoteVolumes.add(p.toPortableString()); } } if (workingDir != null) { IPath p = new Path(workingDir).removeTrailingSeparator(); remoteVolumes.add(p.toPortableString()); } if (commandDir != null) { IPath p = new Path(commandDir).removeTrailingSeparator(); remoteVolumes.add(p.toPortableString()); } builder = builder.volumes(remoteVolumes); } else { // Running daemon on local host. // Add mounts for any directories we need to run the executable. // When we add mount points, we need entries of the form: // hostname:mountname:Z. // In our case, we want all directories mounted as-is so the // executable will run as the user expects. final Set<String> volumes = new TreeSet<>(); if (additionalDirs != null) { for (String dir : additionalDirs) { IPath p = new Path(dir).removeTrailingSeparator(); volumes.add(p.toPortableString() + ":" //$NON-NLS-1$ + p.toPortableString() + ":Z"); //$NON-NLS-1$ } } if (workingDir != null) { IPath p = new Path(workingDir).removeTrailingSeparator(); volumes.add(p.toPortableString() + ":" + p.toPortableString() //$NON-NLS-1$ + ":Z"); //$NON-NLS-1$ } if (commandDir != null) { IPath p = new Path(commandDir).removeTrailingSeparator(); volumes.add(p.toPortableString() + ":" + p.toPortableString() //$NON-NLS-1$ + ":Z"); //$NON-NLS-1$ } List<String> volumeList = new ArrayList<>(volumes); hostBuilder = hostBuilder.binds(volumeList); } final DockerContainerConfig config = builder.build(); // add any port bindings if specified if (portBindingsMap.size() > 0) hostBuilder = hostBuilder.portBindings(portBindingsMap); final IDockerHostConfig hostConfig = hostBuilder.build(); // create the container String containerId = null; try { containerId = ((DockerConnection) connection) .createContainer(config, hostConfig, null); } catch (DockerException | InterruptedException e) { errMsgHolder.setErrorMessage(e.getMessage()); return null; } final String id = containerId; final IDockerConnection conn = connection; Thread t = new Thread(() -> { if (!((DockerConnection) conn).isLocal()) { // if daemon is remote, we need to copy // data over from the host. if (!remoteVolumes.isEmpty()) { CopyVolumesJob job = new CopyVolumesJob(remoteVolumes, conn, id); job.schedule(); try { job.join(); } catch (InterruptedException e) { // ignore } if (job.getResult() != Status.OK_STATUS) return; } } try { ((DockerConnection) conn).startContainer(id, null); } catch (DockerException | InterruptedException e) { // ignore } }); t.start(); return new ContainerCommandProcess(connection, imageName, containerId, remoteVolumes, keepContainer); } /** * Clean up the container used for launching * * @param connectionUri * the URI of the connection used * @param info * the container info */ public void cleanup(String connectionUri, IDockerContainerInfo info) { if (!DockerConnectionManager.getInstance().hasConnections()) { return; } // Try and find the specified connection final IDockerConnection connection = DockerConnectionManager .getInstance().getConnectionByUri(connectionUri); if (connection == null) { return; } try { connection.killContainer(info.id()); } catch (DockerException | InterruptedException e) { // do nothing } } /** * Get the reusable run console for running C/C++ executables in containers. * * @return */ private RunConsole getConsole() { return console; } private void setConsole(RunConsole cons) { console = cons; } /** * Take the command string and parse it into a list of strings. * * @param s * @return list of strings */ private List<String> getCmdList(String s) { ArrayList<String> list = new ArrayList<>(); int length = s.length(); boolean insideQuote1 = false; // single-quote boolean insideQuote2 = false; // double-quote boolean escaped = false; StringBuffer buffer = new StringBuffer(); // Parse the string and break it up into chunks that are // separated by white-space or are quoted. Ignore characters // that have been escaped, including the escape character. for (int i = 0; i < length; ++i) { char c = s.charAt(i); if (escaped) { buffer.append(c); escaped = false; } switch (c) { case '\'': if (!insideQuote2) insideQuote1 = insideQuote1 ^ true; else buffer.append(c); break; case '\"': if (!insideQuote1) insideQuote2 = insideQuote2 ^ true; else buffer.append(c); break; case '\\': escaped = true; break; case ' ': case '\t': case '\r': case '\n': if (insideQuote1 || insideQuote2) buffer.append(c); else { String item = buffer.toString(); buffer.setLength(0); if (item.length() > 0) list.add(item); } break; default: buffer.append(c); break; } } // add last item of string that will be in the buffer String item = buffer.toString(); if (item.length() > 0) list.add(item); return list; } /** * Convert map of environment variables to a {@link List} of KEY=VALUE * String * * @param variables * the entries to manipulate * @return the concatenated key/values for each given variable entry */ private List<String> toList( @SuppressWarnings("rawtypes") final Map variables) { final List<String> result = new ArrayList<>(); if (variables != null) { @SuppressWarnings({ "unchecked", "rawtypes" }) Set<Map.Entry> entries = variables.entrySet(); for (@SuppressWarnings("rawtypes") Map.Entry entry : entries) { final String key = (String) entry.getKey(); final String value = (String) entry.getValue(); final String envEntry = key + "=" + value; //$NON-NLS-1$ result.add(envEntry); } } return result; } /** * Get set of volumes that have been copied from Container to Host as part * of fetchContainerDirs method. * * @param connectionName * - uri of connection used * @param imageName * - name of image used * @return set of paths copied from Container to Host * * @since 3.0 */ public Set<String> getCopiedVolumes(String connectionName, String imageName) { Set<String> copiedSet = new HashSet<>(); if (copiedVolumesMap != null) { Map<String, Set<String>> connectionMap = copiedVolumesMap .get(connectionName); if (connectionMap != null) { Set<String> imageSet = connectionMap.get(imageName); if (imageSet != null) { copiedSet = imageSet; } } } return copiedSet; } }