Fix missing output when running Container commands

- change ContainerCommandProcess destroy() to delay before closing
  the piped std streams to allow output to complete
- also flush the piped streams at the end of the attach thread and
  stop the logging thread after waiting for container completion
- add getImageByTag() method to DockerConnection
- when copying volumes from image in ContainerLauncher,
  keep track of which directories are copied and don't bother if
  they already recorded in a static map for the connection and
  image which we serialize to a file upon exit
- add new getCopiedVolumes() method to ContainerLauncher to get
  the volumes that have been copied for an image
- also keep track of the image id and store in a file so that
  if the image id changes for the repo tag, assume that previous
  copying is null and void and copy all headers again
- don't cause an error if an exception occurs copying as we might
  be attempting to copy a local include path that doesn't exist
  in the image so just ignore it and move to next path to copy  
- fix uses of Objects.toStringHelper to instead use
  MoreObjects.toStringHelper (ImageSearchResultV1,
  ImageSearchResultV2, and RepositoryTag classes)

Change-Id: Ib01ff56d92c50fb7cebf32ce0d277f32ee825fba
diff --git a/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/DockerConnection.java b/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/DockerConnection.java
index bfa46c6..4d6febe 100644
--- a/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/DockerConnection.java
+++ b/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/DockerConnection.java
@@ -626,10 +626,11 @@
 				int delayTime = 100;
 
 				do {
+					outputStream.write("Sleeping".getBytes());
 					Thread.sleep(delayTime);
 					// Second time in loop and following, pause a second to
 					// allow other threads to do meaningful work
-					delayTime = 1000;
+					delayTime = 500;
 					while (stream.hasNext()) {
 						ByteBuffer b = stream.next().content();
 						byte[] bytes = new byte[b.remaining()];
@@ -1094,6 +1095,17 @@
 		return false;
 	}
 
+	public IDockerImage getImageByTag(final String tag) {
+		for (IDockerImage image : getImages()) {
+			for (String repoTag : image.repoTags()) {
+				if (repoTag.equals(tag)) {
+					return image;
+				}
+			}
+		}
+		return null;
+	}
+
 	@Override
 	public IDockerProgressHandler getDefaultBuildImageProgressHandler(
 			String image, int lines) {
diff --git a/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/ImageSearchResultV1.java b/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/ImageSearchResultV1.java
index 08a913b..78610d8 100644
--- a/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/ImageSearchResultV1.java
+++ b/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/ImageSearchResultV1.java
@@ -18,7 +18,7 @@
 
 import com.fasterxml.jackson.annotation.JsonAutoDetect;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.spotify.docker.client.messages.ImageSearchResult;
 
 /**
@@ -99,7 +99,7 @@
 
 	@Override
 	public String toString() {
-		return Objects.toStringHelper(this)
+		return MoreObjects.toStringHelper(this)
 				.add("num_pages", getTotalPages()) //$NON-NLS-1$
 				.add("num_results", getTotalResults()) //$NON-NLS-1$
 				.add("page_size", getPageSize()) //$NON-NLS-1$
diff --git a/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/ImageSearchResultV2.java b/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/ImageSearchResultV2.java
index 11678f1..dc2a11d 100644
--- a/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/ImageSearchResultV2.java
+++ b/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/ImageSearchResultV2.java
@@ -18,7 +18,7 @@
 
 import com.fasterxml.jackson.annotation.JsonAutoDetect;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.spotify.docker.client.messages.ImageSearchResult;
 
 /**
@@ -43,7 +43,7 @@
 
 	@Override
 	public String toString() {
-		return Objects.toStringHelper(this)
+		return MoreObjects.toStringHelper(this)
 				.add("results", getRepositories()).toString(); //$NON-NLS-1$
 	}
 
diff --git a/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/RepositoryTag.java b/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/RepositoryTag.java
index 9cc05c8..cc27ef6 100644
--- a/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/RepositoryTag.java
+++ b/containers/org.eclipse.linuxtools.docker.core/src/org/eclipse/linuxtools/internal/docker/core/RepositoryTag.java
@@ -17,7 +17,7 @@
 
 import com.fasterxml.jackson.annotation.JsonAutoDetect;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 
 /**
  * Repository tag retrieved from Docker Registry version 0.6.3
@@ -84,7 +84,7 @@
 
 	@Override
 	public String toString() {
-		return Objects.toStringHelper(this).add("name", getName()) //$NON-NLS-1$
+		return MoreObjects.toStringHelper(this).add("name", getName()) //$NON-NLS-1$
 				.add("layer", getLayer()).toString(); //$NON-NLS-1$
 	}
 
diff --git a/containers/org.eclipse.linuxtools.docker.ui/src/org/eclipse/linuxtools/docker/ui/launch/ContainerLauncher.java b/containers/org.eclipse.linuxtools.docker.ui/src/org/eclipse/linuxtools/docker/ui/launch/ContainerLauncher.java
index e86bc4e..ae97049 100644
--- a/containers/org.eclipse.linuxtools.docker.ui/src/org/eclipse/linuxtools/docker/ui/launch/ContainerLauncher.java
+++ b/containers/org.eclipse.linuxtools.docker.ui/src/org/eclipse/linuxtools/docker/ui/launch/ContainerLauncher.java
@@ -10,10 +10,18 @@
  *******************************************************************************/
 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;
@@ -34,6 +42,7 @@
 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;
@@ -67,10 +76,15 @@
 	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$
@@ -140,12 +154,14 @@
 		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 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,
@@ -155,17 +171,61 @@
 			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.getString(COPY_VOLUMES_FROM_DESC),
+					Messages.getFormattedString(COPY_VOLUMES_FROM_DESC, image),
 					volumes.size());
 			String containerId = null;
 			try {
+				IDockerImage dockerImage = ((DockerConnection) connection)
+						.getImageByTag(image);
+				IPath imageFilePath = target.append(".IMAGE_ID");
+				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);
+						.cmd("/bin/sh").image(image); //$NON-NLS-1$
 				IDockerContainerConfig config = builder.build();
 				DockerHostConfig.Builder hostBuilder = new DockerHostConfig.Builder();
 				IDockerHostConfig hostConfig = hostBuilder.build();
@@ -176,7 +236,12 @@
 						monitor.done();
 						return Status.CANCEL_STATUS;
 					}
-					if (volume.contains("${ProjName}")) {
+					if (volume.contains("${ProjName}")) { //$NON-NLS-1$
+						monitor.worked(1);
+						continue;
+					}
+					if (dirList.contains(volume)) {
+						monitor.worked(1);
 						continue;
 					}
 					try {
@@ -188,6 +253,10 @@
 						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
@@ -231,18 +300,18 @@
 						}
 						k.close();
 					} catch (final DockerException e) {
-						Display.getDefault()
-								.syncExec(() -> MessageDialog.openError(
-										PlatformUI.getWorkbench()
-												.getActiveWorkbenchWindow()
-												.getShell(),
-										Messages.getFormattedString(
-												ERROR_COPYING_VOLUME,
-												new String[] { volume, target
-														.toPortableString() }),
-										e.getCause() != null
-												? e.getCause().getMessage()
-												: e.getMessage()));
+						// Display.getDefault()
+						// .syncExec(() -> MessageDialog.openError(
+						// PlatformUI.getWorkbench()
+						// .getActiveWorkbenchWindow()
+						// .getShell(),
+						// Messages.getFormattedString(
+						// ERROR_COPYING_VOLUME,
+						// new String[] { volume, target
+						// .toPortableString() }),
+						// e.getCause() != null
+						// ? e.getCause().getMessage()
+						// : e.getMessage()));
 					}
 				}
 			} catch (InterruptedException e) {
@@ -282,6 +351,58 @@
 		}
 	}
 
+	@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.
 	 * 
@@ -1063,4 +1184,23 @@
 
 	}
 
+	/**
+	 * @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;
+	}
+
 }
diff --git a/containers/org.eclipse.linuxtools.docker.ui/src/org/eclipse/linuxtools/internal/docker/ui/launch/ContainerCommandProcess.java b/containers/org.eclipse.linuxtools.docker.ui/src/org/eclipse/linuxtools/internal/docker/ui/launch/ContainerCommandProcess.java
index 2f2a6b5..7c5e06a 100644
--- a/containers/org.eclipse.linuxtools.docker.ui/src/org/eclipse/linuxtools/internal/docker/ui/launch/ContainerCommandProcess.java
+++ b/containers/org.eclipse.linuxtools.docker.ui/src/org/eclipse/linuxtools/internal/docker/ui/launch/ContainerCommandProcess.java
@@ -37,6 +37,8 @@
 					PipedOutputStream pipedStderr = new PipedOutputStream(
 							stderr)) {
 				connection.attachLog(containerId, pipedStdout, pipedStderr);
+				pipedStdout.flush();
+				pipedStderr.flush();
 			} catch (DockerException | InterruptedException | IOException e) {
 				// do nothing but close output streams
 			}
@@ -45,30 +47,18 @@
 		// start the thread
 		this.thread = new Thread(logContainer);
 		this.thread.start();
-		// Runnable watchContainer = () -> {
-		// try {
-		// IDockerContainerExit exit = connection
-		// .waitForContainer(containerId);
-		// } catch (DockerException | InterruptedException e) {
-		// // do nothing
-		// }
-		//
-		// try {
-		// System.out.println("watchcontainer finished");
-		// this.stdout.close();
-		// this.stderr.close();
-		// this.thread.interrupt();
-		// } catch (IOException e) {
-		// Activator.log(e);
-		// }
-		// };
-		// // kick off a thread to stop logging
-		// new Thread(watchContainer).start();
 	}
 
 	@Override
 	public void destroy() {
 		try {
+			try {
+				// TODO: see if there is a better way of draining the
+				// container output before closing the streams
+				Thread.sleep(1000);
+			} catch (InterruptedException e1) {
+				// ignore
+			}
 			this.stdout.close();
 			this.stderr.close();
 		} catch (IOException e) {
@@ -115,6 +105,7 @@
 		try {
 			IDockerContainerExit exit = connection
 					.waitForContainer(containerId);
+			connection.stopLoggingThread(containerId);
 			return exit.statusCode();
 		} catch (DockerException e) {
 			return -1;