| /******************************************************************************* |
| * 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 static final String ERROR_COPYING_VOLUME = |
| // "ContainerLaunch.copyVolumesFromJob.error"; //$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; |
| } |
| 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 |
| e.printStackTrace(); |
| } |
| } catch (IOException e) { |
| // will handle this below |
| } |
| } |
| } |
| if (copiedVolumesMap == null) { |
| copiedVolumesMap = new HashMap<>(); |
| } |
| } |
| } |
| |
| /** |
| * Perform a launch of a command in a container. |
| * |
| * @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. |
| * |
| * @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. |
| * |
| * @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) { |
| |
| 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).securityOpt("seccomp:unconfined"); //$NON-NLS-1$ |
| |
| |
| 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; |
| } |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * @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, |
| 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() { |
| // if (console == null) { |
| // console = RunConsole.getContainerLessConsole(); |
| // } |
| 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; |
| |
| } |
| |
| /** |
| * @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; |
| } |
| |
| } |