From c282d533494da20fd57b390d78dcde2334ec1779 Mon Sep 17 00:00:00 2001 From: pabloFuente Date: Fri, 29 Mar 2019 12:26:37 +0100 Subject: [PATCH] openvidu-server: DockerManager refactoring --- .../io/openvidu/client/OpenViduException.java | 2 +- .../io/openvidu/server/OpenViduServer.java | 2 +- .../service/ComposedRecordingService.java | 105 ++++------- .../recording/service/RecordingManager.java | 93 ++-------- .../openvidu/server/utils/DockerManager.java | 173 ++++++++++++++++++ 5 files changed, 218 insertions(+), 157 deletions(-) create mode 100644 openvidu-server/src/main/java/io/openvidu/server/utils/DockerManager.java diff --git a/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java b/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java index b0ca151e..91810aa9 100644 --- a/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java +++ b/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java @@ -48,7 +48,7 @@ public class OpenViduException extends JsonRpcErrorException { SIGNAL_FORMAT_INVALID_ERROR_CODE(600), SIGNAL_TO_INVALID_ERROR_CODE(601), SIGNAL_MESSAGE_INVALID_ERROR_CODE(602), - RECORDING_ENABLED_BUT_DOCKER_NOT_FOUND(709), RECORDING_PATH_NOT_VALID(708), RECORDING_FILE_EMPTY_ERROR(707), + DOCKER_NOT_FOUND(709), RECORDING_PATH_NOT_VALID(708), RECORDING_FILE_EMPTY_ERROR(707), RECORDING_DELETE_ERROR_CODE(706), RECORDING_LIST_ERROR_CODE(705), RECORDING_STOP_ERROR_CODE(704), RECORDING_START_ERROR_CODE(703), RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701); diff --git a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java index 0c77dc41..597176e3 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java +++ b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java @@ -257,7 +257,7 @@ public class OpenViduServer implements JsonRpcConfigurer { this.recordingManager().initializeRecordingManager(); } catch (OpenViduException e) { String finalErrorMessage = ""; - if (e.getCodeValue() == Code.RECORDING_ENABLED_BUT_DOCKER_NOT_FOUND.getValue()) { + if (e.getCodeValue() == Code.DOCKER_NOT_FOUND.getValue()) { finalErrorMessage = "Error connecting to Docker daemon. Enabling OpenVidu recording module requires Docker"; } else if (e.getCodeValue() == Code.RECORDING_PATH_NOT_VALID.getValue()) { finalErrorMessage = "Error initializing recording path \"" diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java index 2923ea17..06eab02a 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java @@ -30,18 +30,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.command.CreateContainerCmd; -import com.github.dockerjava.api.command.CreateContainerResponse; -import com.github.dockerjava.api.command.ExecCreateCmdResponse; -import com.github.dockerjava.api.exception.ConflictException; -import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; -import com.github.dockerjava.core.DefaultDockerClientConfig; -import com.github.dockerjava.core.DockerClientBuilder; -import com.github.dockerjava.core.DockerClientConfig; -import com.github.dockerjava.core.command.ExecStartResultCallback; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; @@ -57,6 +47,7 @@ import io.openvidu.server.kurento.core.KurentoSession; import io.openvidu.server.recording.CompositeWrapper; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingInfoUtils; +import io.openvidu.server.utils.DockerManager; public class ComposedRecordingService extends RecordingService { @@ -66,12 +57,11 @@ public class ComposedRecordingService extends RecordingService { private Map sessionsContainers = new ConcurrentHashMap<>(); private Map composites = new ConcurrentHashMap<>(); - DockerClient dockerClient; + private DockerManager dockerManager; public ComposedRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) { super(recordingManager, openviduConfig); - DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); - this.dockerClient = DockerClientBuilder.getInstance(config).build(); + this.dockerManager = new DockerManager(); } @Override @@ -160,7 +150,20 @@ public class ComposedRecordingService extends RecordingService { String containerId; try { - containerId = this.runRecordingContainer(envs, "recording_" + recording.getId()); + final String container = RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG; + final String containerName = "recording_" + recording.getId(); + Volume volume1 = new Volume("/recordings"); + Volume volume2 = new Volume("/dev/shm"); + List volumes = new ArrayList<>(); + volumes.add(volume1); + volumes.add(volume2); + Bind bind1 = new Bind(openviduConfig.getOpenViduRecordingPath(), volume1); + Bind bind2 = new Bind("/dev/shm", volume2); + List binds = new ArrayList<>(); + binds.add(bind1); + binds.add(bind2); + containerId = dockerManager.runContainer(container, containerName, volumes, binds, envs); + containers.put(containerId, containerName); } catch (Exception e) { this.cleanRecordingMaps(recording); throw this.failStartRecording(session, recording, @@ -251,8 +254,9 @@ public class ComposedRecordingService extends RecordingService { } else { log.warn("Removing container {} for closed session {}...", containerIdAux, session.getSessionId()); - dockerClient.stopContainerCmd(containerIdAux).exec(); - this.removeDockerContainer(containerIdAux); + dockerManager.stopDockerContainer(containerIdAux); + dockerManager.removeDockerContainer(containerIdAux, false); + containers.remove(containerId); containerClosed = true; log.warn("Container {} for closed session {} succesfully stopped and removed", containerIdAux, session.getSessionId()); @@ -272,36 +276,25 @@ public class ComposedRecordingService extends RecordingService { } else { // Gracefully stop ffmpeg process - ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withAttachStdout(true) - .withAttachStderr(true).withCmd("bash", "-c", "echo 'q' > stop").exec(); try { - dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ExecStartResultCallback()) - .awaitCompletion(); - } catch (InterruptedException e) { - e.printStackTrace(); + dockerManager.runCommandInContainer(containerId, "echo 'q' > stop"); + } catch (InterruptedException e1) { + e1.printStackTrace(); } // Wait for the container to be gracefully self-stopped - CountDownLatch latch = new CountDownLatch(1); - WaitForContainerStoppedCallback callback = new WaitForContainerStoppedCallback(latch); - dockerClient.waitContainerCmd(containerId).exec(callback); - - boolean stopped = false; + final int timeOfWait = 30; try { - stopped = latch.await(30, TimeUnit.SECONDS); - } catch (InterruptedException e) { + dockerManager.waitForContainerStopped(containerId, timeOfWait); + } catch (Exception e) { failRecordingCompletion(recording, containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE, - "The recording completion process has been unexpectedly interrupted")); - } - if (!stopped) { - failRecordingCompletion(recording, containerId, - new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE, - "The recording completion process couldn't finish in 30 seconds")); + "The recording completion process couldn't finish in " + timeOfWait + " seconds")); } // Remove container - this.removeDockerContainer(containerId); + dockerManager.removeDockerContainer(containerId, false); + containers.remove(containerId); // Update recording attributes reading from video report file try { @@ -390,41 +383,6 @@ public class ComposedRecordingService extends RecordingService { return recording; } - private String runRecordingContainer(List envs, String containerName) throws Exception { - Volume volume1 = new Volume("/recordings"); - Volume volume2 = new Volume("/dev/shm"); - CreateContainerCmd cmd = dockerClient - .createContainerCmd(RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG) - .withName(containerName).withEnv(envs).withNetworkMode("host").withVolumes(volume1) - .withBinds(new Bind(openviduConfig.getOpenViduRecordingPath(), volume1), new Bind("/dev/shm", volume2)); - CreateContainerResponse container = null; - try { - container = cmd.exec(); - dockerClient.startContainerCmd(container.getId()).exec(); - containers.put(container.getId(), containerName); - log.info("Container ID: {}", container.getId()); - return container.getId(); - } catch (ConflictException e) { - log.error( - "The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream", - containerName); - throw e; - } catch (NotFoundException e) { - log.error("Docker image {} couldn't be found in docker host", - RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG); - throw e; - } - } - - private void removeDockerContainer(String containerId) { - dockerClient.removeContainerCmd(containerId).exec(); - containers.remove(containerId); - } - - private void stopDockerContainer(String containerId) { - dockerClient.stopContainerCmd(containerId).exec(); - } - private void waitForVideoFileNotEmpty(Recording recording) throws OpenViduException { boolean isPresent = false; int i = 1; @@ -451,8 +409,9 @@ public class ComposedRecordingService extends RecordingService { private void failRecordingCompletion(Recording recording, String containerId, OpenViduException e) throws OpenViduException { recording.setStatus(io.openvidu.java.client.Recording.Status.failed); - this.stopDockerContainer(containerId); - this.removeDockerContainer(containerId); + dockerManager.stopDockerContainer(containerId); + dockerManager.removeDockerContainer(containerId, true); + containers.remove(containerId); throw e; } diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java index fc3a80bf..ea1b84bd 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java @@ -37,8 +37,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; -import javax.ws.rs.ProcessingException; - import org.apache.commons.io.FileUtils; import org.kurento.client.ErrorEvent; import org.kurento.client.EventListener; @@ -51,11 +49,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import com.github.dockerjava.api.exception.DockerClientException; -import com.github.dockerjava.api.exception.InternalServerErrorException; -import com.github.dockerjava.api.exception.NotFoundException; -import com.github.dockerjava.api.model.Container; -import com.github.dockerjava.core.command.PullImageResultCallback; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -74,6 +67,7 @@ import io.openvidu.server.kurento.KurentoClientSessionInfo; import io.openvidu.server.kurento.OpenViduKurentoClientSessionInfo; import io.openvidu.server.recording.Recording; import io.openvidu.server.utils.CustomFileManager; +import io.openvidu.server.utils.DockerManager; @Service public class RecordingManager { @@ -83,6 +77,7 @@ public class RecordingManager { RecordingService recordingService; private ComposedRecordingService composedRecordingService; private SingleStreamRecordingService singleStreamRecordingService; + private DockerManager dockerManager; @Autowired protected SessionEventsHandler sessionHandler; @@ -105,7 +100,7 @@ public class RecordingManager { Runtime.getRuntime().availableProcessors()); static final String RECORDING_ENTITY_FILE = ".recording."; - static final String IMAGE_NAME = "openvidu/openvidu-recording"; + public static final String IMAGE_NAME = "openvidu/openvidu-recording"; static String IMAGE_TAG; private static final List LAST_PARTICIPANT_LEFT_REASONS = Arrays @@ -124,6 +119,7 @@ public class RecordingManager { RecordingManager.IMAGE_TAG = openviduConfig.getOpenViduRecordingVersion(); + this.dockerManager = new DockerManager(); this.composedRecordingService = new ComposedRecordingService(this, openviduConfig); this.singleStreamRecordingService = new SingleStreamRecordingService(this, openviduConfig); @@ -133,7 +129,7 @@ public class RecordingManager { this.checkRecordingRequirements(this.openviduConfig.getOpenViduRecordingPath(), this.openviduConfig.getOpenviduRecordingCustomLayout()); - if (this.recordingImageExistsLocally()) { + if (dockerManager.dockerImageExistsLocally(IMAGE_NAME + ":" + IMAGE_TAG)) { log.info("Docker image already exists locally"); } else { Thread t = new Thread(() -> { @@ -150,7 +146,7 @@ public class RecordingManager { } }); t.start(); - this.downloadRecordingImage(); + dockerManager.downloadDockerImage(IMAGE_NAME + ":" + IMAGE_TAG); t.interrupt(); try { t.join(); @@ -161,12 +157,15 @@ public class RecordingManager { } // Clean any stranded openvidu/openvidu-recording container on startup - this.removeExistingRecordingContainers(); + dockerManager.cleanStrandedContainers(RecordingManager.IMAGE_NAME); } public void checkRecordingRequirements(String openviduRecordingPath, String openviduRecordingCustomLayout) throws OpenViduException { - this.checkDockerEnabled(); + if (dockerManager == null) { + this.dockerManager = new DockerManager(); + } + dockerManager.checkDockerEnabled(openviduConfig.getSpringProfile()); this.checkRecordingPaths(openviduRecordingPath, openviduRecordingCustomLayout); } @@ -441,53 +440,6 @@ public class RecordingManager { return recording; } - private void removeExistingRecordingContainers() { - List existingContainers = this.composedRecordingService.dockerClient.listContainersCmd() - .withShowAll(true).exec(); - for (Container container : existingContainers) { - if (container.getImage().startsWith(RecordingManager.IMAGE_NAME)) { - log.info("Stranded openvidu/openvidu-recording Docker container ({}) removed on startup", - container.getId()); - this.composedRecordingService.dockerClient.removeContainerCmd(container.getId()).withForce(true).exec(); - } - } - } - - private boolean recordingImageExistsLocally() { - boolean imageExists = false; - try { - this.composedRecordingService.dockerClient.inspectImageCmd(IMAGE_NAME + ":" + IMAGE_TAG).exec(); - imageExists = true; - } catch (NotFoundException nfe) { - imageExists = false; - } catch (ProcessingException e) { - throw e; - } catch (NullPointerException e) { - // Restarting openvidu-server from openvidu.recording=false - // ComposedRecordingService was not initialized - this.composedRecordingService = new ComposedRecordingService(this, openviduConfig); - return this.recordingImageExistsLocally(); - } - return imageExists; - } - - private void downloadRecordingImage() { - try { - this.composedRecordingService.dockerClient.pullImageCmd(IMAGE_NAME + ":" + IMAGE_TAG) - .exec(new PullImageResultCallback()).awaitSuccess(); - } catch (NotFoundException | InternalServerErrorException e) { - if (recordingImageExistsLocally()) { - log.info("Docker image '{}' exists locally", IMAGE_NAME + ":" + IMAGE_TAG); - } else { - throw e; - } - } catch (DockerClientException e) { - log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution", - IMAGE_NAME + ":" + IMAGE_TAG); - throw e; - } - } - private Recording getRecordingFromHost(String recordingId) { log.info(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/" + RecordingManager.RECORDING_ENTITY_FILE + recordingId); @@ -542,29 +494,6 @@ public class RecordingManager { return fileNamesNoExtension; } - private void checkDockerEnabled() throws OpenViduException { - try { - this.recordingImageExistsLocally(); - } catch (ProcessingException exception) { - String message = "Exception connecting to Docker daemon: "; - if ("docker".equals(openviduConfig.getSpringProfile())) { - final String NEW_LINE = System.getProperty("line.separator"); - message += "make sure you include the following flags in your \"docker run\" command:" + NEW_LINE - + " -e openvidu.recording.path=/YOUR/PATH/TO/VIDEO/FILES" + NEW_LINE - + " -e MY_UID=$(id -u $USER)" + NEW_LINE + " -v /var/run/docker.sock:/var/run/docker.sock" - + NEW_LINE + " -v /YOUR/PATH/TO/VIDEO/FILES:/YOUR/PATH/TO/VIDEO/FILES" + NEW_LINE; - } else { - message += "you need Docker CE installed in this machine to enable OpenVidu recording service. " - + "If Docker CE is already installed, make sure to add OpenVidu Server user to " - + "\"docker\" group: " + System.lineSeparator() + " 1) $ sudo usermod -aG docker $USER" - + System.lineSeparator() - + " 2) Log out and log back to the host to reevaluate group membership"; - } - log.error(message); - throw new OpenViduException(Code.RECORDING_ENABLED_BUT_DOCKER_NOT_FOUND, message); - } - } - private void checkRecordingPaths(String openviduRecordingPath, String openviduRecordingCustomLayout) throws OpenViduException { log.info("Initializing recording paths"); diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/DockerManager.java b/openvidu-server/src/main/java/io/openvidu/server/utils/DockerManager.java new file mode 100644 index 00000000..f7a7a2ad --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/DockerManager.java @@ -0,0 +1,173 @@ +/* + * (C) Copyright 2017-2019 OpenVidu (https://openvidu.io/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.openvidu.server.utils; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.ProcessingException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.ExecCreateCmdResponse; +import com.github.dockerjava.api.exception.ConflictException; +import com.github.dockerjava.api.exception.DockerClientException; +import com.github.dockerjava.api.exception.InternalServerErrorException; +import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.Volume; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientBuilder; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.command.ExecStartResultCallback; +import com.github.dockerjava.core.command.PullImageResultCallback; + +import io.openvidu.client.OpenViduException; +import io.openvidu.client.OpenViduException.Code; +import io.openvidu.server.recording.service.WaitForContainerStoppedCallback; + +public class DockerManager { + + private static final Logger log = LoggerFactory.getLogger(DockerManager.class); + + DockerClient dockerClient; + + public DockerManager() { + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + this.dockerClient = DockerClientBuilder.getInstance(config).build(); + } + + public void downloadDockerImage(String image) { + try { + this.dockerClient.pullImageCmd(image).exec(new PullImageResultCallback()).awaitSuccess(); + } catch (NotFoundException | InternalServerErrorException e) { + if (dockerImageExistsLocally(image)) { + log.info("Docker image '{}' exists locally", image); + } else { + throw e; + } + } catch (DockerClientException e) { + log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution", image); + throw e; + } + } + + public boolean dockerImageExistsLocally(String image) throws ProcessingException { + boolean imageExists = false; + try { + this.dockerClient.inspectImageCmd(image).exec(); + imageExists = true; + } catch (NotFoundException nfe) { + imageExists = false; + } catch (ProcessingException e) { + throw e; + } + return imageExists; + } + + public void checkDockerEnabled(String springProfile) throws OpenViduException { + try { + this.dockerImageExistsLocally("hello-world"); + log.info("Docker is installed and enabled"); + } catch (ProcessingException exception) { + String message = "Exception connecting to Docker daemon: "; + if ("docker".equals(springProfile)) { + final String NEW_LINE = System.getProperty("line.separator"); + message += "make sure you include the following flags in your \"docker run\" command:" + NEW_LINE + + " -e openvidu.recording.path=/YOUR/PATH/TO/VIDEO/FILES" + NEW_LINE + + " -e MY_UID=$(id -u $USER)" + NEW_LINE + " -v /var/run/docker.sock:/var/run/docker.sock" + + NEW_LINE + " -v /YOUR/PATH/TO/VIDEO/FILES:/YOUR/PATH/TO/VIDEO/FILES" + NEW_LINE; + } else { + message += "you need Docker CE installed in this machine to enable OpenVidu recording service. " + + "If Docker CE is already installed, make sure to add OpenVidu Server user to " + + "\"docker\" group: " + System.lineSeparator() + " 1) $ sudo usermod -aG docker $USER" + + System.lineSeparator() + + " 2) Log out and log back to the host to reevaluate group membership"; + } + log.error(message); + throw new OpenViduException(Code.DOCKER_NOT_FOUND, message); + } + } + + public String runContainer(String container, String containerName, List volumes, List binds, + List envs) throws Exception { + CreateContainerCmd cmd = dockerClient.createContainerCmd(container).withName(containerName).withEnv(envs) + .withNetworkMode("host").withVolumes(volumes).withBinds(binds); + CreateContainerResponse response = null; + try { + response = cmd.exec(); + dockerClient.startContainerCmd(response.getId()).exec(); + log.info("Container ID: {}", response.getId()); + return response.getId(); + } catch (ConflictException e) { + log.error( + "The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream", + containerName); + throw e; + } catch (NotFoundException e) { + log.error("Docker image {} couldn't be found in docker host", container); + throw e; + } + } + + public void removeDockerContainer(String containerId, boolean force) { + dockerClient.removeContainerCmd(containerId).withForce(force).exec(); + } + + public void stopDockerContainer(String containerId) { + dockerClient.stopContainerCmd(containerId).exec(); + } + + public void cleanStrandedContainers(String imageName) { + List existingContainers = this.dockerClient.listContainersCmd().withShowAll(true).exec(); + for (Container container : existingContainers) { + if (container.getImage().startsWith(imageName)) { + log.info("Stranded {} Docker container ({}) removed on startup", imageName, container.getId()); + this.dockerClient.removeContainerCmd(container.getId()).withForce(true).exec(); + } + } + } + + public void runCommandInContainer(String containerId, String command) throws InterruptedException { + ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withAttachStdout(true) + .withAttachStderr(true).withCmd("bash", "-c", command).exec(); + dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ExecStartResultCallback()).awaitCompletion(); + } + + public void waitForContainerStopped(String containerId, int secondsOfWait) throws Exception { + CountDownLatch latch = new CountDownLatch(1); + WaitForContainerStoppedCallback callback = new WaitForContainerStoppedCallback(latch); + dockerClient.waitContainerCmd(containerId).exec(callback); + boolean stopped = false; + try { + stopped = latch.await(secondsOfWait, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw e; + } + if (!stopped) { + throw new Exception(); + } + } + +}