diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 8b73f86..6544037 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -18,15 +18,6 @@ jobs: image_tag: ${{ steps.meta.outputs.image_tag }} steps: - uses: actions/checkout@v4 - - - name: Create /tmp/oci-tools directory on runner - run: mkdir /tmp/oci-tools - - - name: Prepare and Build OCI tools on runner - run: | - cd emucon-tools/ - . ./bootstrap.sh - ./installer/install-oci-tools.sh --destination /tmp/oci-tools - name: Extract metadata (tags, labels) id: meta diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..d1acb25 --- /dev/null +++ b/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash + + +apt-get update && apt-get install -y curl unzip sudo \ + && rm -rf /var/lib/apt/lists/* + +mkdir /tmp/oci-tools +./emucon-tools/installer/install-oci-tools.sh --destination /tmp/oci-tools \ No newline at end of file diff --git a/commons/src/main/java/de/bwl/bwfla/emucomp/common/EmulationEnvironmentHelper.java b/commons/src/main/java/de/bwl/bwfla/emucomp/common/EmulationEnvironmentHelper.java index c5870e1..8eee3dd 100755 --- a/commons/src/main/java/de/bwl/bwfla/emucomp/common/EmulationEnvironmentHelper.java +++ b/commons/src/main/java/de/bwl/bwfla/emucomp/common/EmulationEnvironmentHelper.java @@ -135,15 +135,14 @@ public static MachineConfiguration clean(final MachineConfiguration original, bo continue; } -// if (r instanceof ImageArchiveBinding) -// { -// -// if(env.checkpointBindingId == null || env.checkpointBindingId.isEmpty()) { -// ImageArchiveBinding iab = (ImageArchiveBinding) r; -// if (iab.getId().equals("emucon-rootfs")) -// it.remove(); -// } -// } + if (r instanceof ImageArchiveBinding) + { + if(env.checkpointBindingId == null || env.checkpointBindingId.isEmpty()) { + ImageArchiveBinding iab = (ImageArchiveBinding) r; + if (iab.getId().equals("emucon-rootfs")) + it.remove(); + } + } // resource was volatile (but not in use by a drive), remove it, too for (AbstractDataResource origRes : original.getAbstractDataResource()) { @@ -181,26 +180,6 @@ private static String _registerDataSource(String conf, String ref, String type) } } -// /** -// * Drives capable to accept ready made images. -// * -// * @return -// */ -// public static List getImageDrives(MachineConfiguration env) { -// List emptyDrives = new ArrayList<>(); -// Iterator iterator = env.getDrive().iterator(); -// -// while (iterator.hasNext()) { -// Drive d = iterator.next(); -// if (d.getData() == null || d.getData().isEmpty()) { -// Drive.DriveType type = d.getType(); -// emptyDrives.add(type.name()); -// } -// } -// -// return emptyDrives; -// } - /** * Drives require a file system helper (FS annotation is required) * diff --git a/commons/src/main/java/de/bwl/bwfla/emucomp/common/FileCollection.java b/commons/src/main/java/de/bwl/bwfla/emucomp/common/FileCollection.java index b694266..27c460c 100755 --- a/commons/src/main/java/de/bwl/bwfla/emucomp/common/FileCollection.java +++ b/commons/src/main/java/de/bwl/bwfla/emucomp/common/FileCollection.java @@ -1,113 +1,94 @@ package de.bwl.bwfla.emucomp.common; import de.bwl.bwfla.emucomp.common.utils.jaxb.JaxbType; +import lombok.Getter; import javax.xml.bind.JAXBException; import javax.xml.bind.annotation.*; import java.util.ArrayList; import java.util.List; +@Getter @XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "fileCollection", namespace="http://bwfla.bwl.de/common/datatypes") +@XmlType(name = "fileCollection", namespace = "http://bwfla.bwl.de/common/datatypes") @XmlRootElement(namespace = "http://bwfla.bwl.de/common/datatypes") public class FileCollection extends JaxbType { - @XmlElement(name="file", namespace="http://bwfla.bwl.de/common/datatypes") - public List files = new ArrayList(); - - @XmlElement(namespace="http://bwfla.bwl.de/common/datatypes") - public String id; - - @XmlElement - private String archive; - - @XmlElement - private String label; - - public FileCollection() - { - id = null; - } - - public FileCollection(String id) - { - this.id = id; - } - + @XmlElement(name = "file", namespace = "http://bwfla.bwl.de/common/datatypes") + public List files = new ArrayList(); + + @XmlElement(namespace = "http://bwfla.bwl.de/common/datatypes") + public String id; + + @XmlElement + private String archive; + + @XmlElement + private String label; + + public FileCollection() { + id = null; + } + + public FileCollection(String id) { + this.id = id; + } + + public FileCollection(String id, String archive, String label) { + this.id = id; + this.archive = archive; + this.label = label; + } + public static FileCollection fromValue(String data) throws JAXBException { return JaxbType.fromValue(data, FileCollection.class); } - public FileCollectionEntry getDefaultEntry() - { - for(FileCollectionEntry fc : files) - if(fc.isDefault()) - return fc; - - return files.get(0); - } - - public FileCollection copy() - { - try { - return fromValue(this.value()); - } - catch(JAXBException e) { - // impossible - return null; - } + public FileCollectionEntry getDefaultEntry() { + for (FileCollectionEntry fc : files) + if (fc.isDefault()) + return fc; + + return files.get(0); } - public void update() - { - for (final var file : files) { - file.setArchive(this.archive); - file.setObjectId(this.id); - } - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public String getArchive() - { - return archive; - } - - public void setArchive(String archive) - { - this.archive = archive; - } - - public FileCollectionEntry find(String resourceId) - { - for (final var resource : files) { - if (!resourceId.equalsIgnoreCase(resource.getId())) - continue; - - return resource; - } - - return null; - } - - public String resolve(String exportUrlPrefix, String resourceId) - { - final var resource = this.find(resourceId); - return (resource != null) ? resource.resolve(exportUrlPrefix) : null; - } - - public boolean contains(Binding.ResourceType rt) - { - for (final var resource : files) { - if (resource.getResourceType() == rt) - return true; - } - - return false; - } + public FileCollection copy() { + try { + return fromValue(this.value()); + } catch (JAXBException e) { + // impossible + return null; + } + } + + public void update() { + for (final var file : files) { + file.setArchive(this.archive); + file.setObjectId(this.id); + } + } + + public FileCollectionEntry find(String resourceId) { + for (final var resource : files) { + if (!resourceId.equalsIgnoreCase(resource.getId())) + continue; + + return resource; + } + + return null; + } + + public String resolve(String exportUrlPrefix, String resourceId) { + final var resource = this.find(resourceId); + return (resource != null) ? resource.resolve(exportUrlPrefix) : null; + } + + public boolean contains(Binding.ResourceType rt) { + for (final var resource : files) { + if (resource.getResourceType() == rt) + return true; + } + + return false; + } } diff --git a/commons/src/main/java/de/bwl/bwfla/emucomp/common/MachineConfiguration.java b/commons/src/main/java/de/bwl/bwfla/emucomp/common/MachineConfiguration.java index bdb319c..80e6cdf 100755 --- a/commons/src/main/java/de/bwl/bwfla/emucomp/common/MachineConfiguration.java +++ b/commons/src/main/java/de/bwl/bwfla/emucomp/common/MachineConfiguration.java @@ -21,6 +21,9 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import de.bwl.bwfla.emucomp.common.utils.jaxb.JaxbType; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; import javax.xml.bind.JAXBException; import javax.xml.bind.annotation.*; @@ -30,6 +33,8 @@ import java.util.logging.Logger; +@Getter +@Setter @JsonTypeName("emulationEnvironment") @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "emulationEnvironment", namespace = "http://bwfla.bwl.de/common/datatypes", propOrder = { @@ -57,6 +62,7 @@ public class MachineConfiguration protected EmulatorSpec emulator; @XmlElement(namespace = "http://bwfla.bwl.de/common/datatypes", required = false) protected String model; + @lombok.Setter @XmlElement(name = "ui_options", namespace = "http://bwfla.bwl.de/common/datatypes") protected UiOptions uiOptions; @XmlElement(namespace = "http://bwfla.bwl.de/common/datatypes") @@ -73,6 +79,8 @@ public class MachineConfiguration @XmlElementRef(name = "objectArchiveBinding", type = ObjectArchiveBinding.class, namespace = "http://bwfla.bwl.de/common/datatypes")}) protected List abstractDataResource; + protected List attachedFiles; + @XmlElement(namespace = "http://bwfla.bwl.de/common/datatypes") protected NativeConfig nativeConfig; @@ -108,48 +116,9 @@ protected MachineConfiguration(MachineConfigurationTemplate template) { public MachineConfiguration() { } - public String getOperatingSystemId() { - return operatingSystemId; - } - - public void setOperatingSystemId(String operatingSystemId) { - this.operatingSystemId = operatingSystemId; - } - @XmlElement(namespace = "http://bwfla.bwl.de/common/datatypes", name = "installedSoftwareId") protected List installedSoftwareIds = new ArrayList(); - public String getArch() { - return arch; - } - - public void setArch(String value) { - this.arch = value; - } - - public String getModel() { - return model; - } - - public void setModel(String value) { - this.model = value; - } - - public EmulatorSpec getEmulator() { - return emulator; - } - - public void setEmulator(EmulatorSpec value) { - this.emulator = value; - } - - public UiOptions getUiOptions() { - return uiOptions; - } - - public void setUiOptions(UiOptions value) { - this.uiOptions = value; - } public List getDrive() { if (drive == null) { @@ -158,10 +127,6 @@ public List getDrive() { return this.drive; } - public void setDrive(List drives) { - this.drive = drives; - } - public List getNic() { if (nic == null) { nic = new ArrayList(); @@ -169,10 +134,6 @@ public List getNic() { return this.nic; } - public void setAbstractDataResource(List abstractDataResource) { - this.abstractDataResource = abstractDataResource; - } - public List getAbstractDataResource() { if (abstractDataResource == null) { abstractDataResource = new ArrayList(); @@ -180,23 +141,6 @@ public List getAbstractDataResource() { return this.abstractDataResource; } - - public NativeConfig getNativeConfig() { - return nativeConfig; - } - - public void setNativeConfig(NativeConfig value) { - this.nativeConfig = value; - } - - public List getInstalledSoftwareIds() { - return installedSoftwareIds; - } - - public void setInstalledSoftwareIds(List ids) { - this.installedSoftwareIds = ids; - } - public boolean hasCheckpointBindingId() { return (checkpointBindingId != null && !checkpointBindingId.isEmpty()); } @@ -213,26 +157,6 @@ public String getCheckpointBindingId(boolean stripped) { return checkpointBindingId; } - public void setCheckpointBindingId(String checkpointId) { - this.checkpointBindingId = checkpointId; - } - - public String getOutputBindingId() { - return outputBindingId; - } - - public boolean isLinuxRuntime() { - return isLinuxRuntime; - } - - public void setLinuxRuntime(boolean linuxRuntime) { - isLinuxRuntime = linuxRuntime; - } - - public void setOutputBindingId(String bindingId) { - this.outputBindingId = bindingId; - } - @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "", propOrder = { "value" diff --git a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/NodeManager.java b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/NodeManager.java index f4f0674..d18772c 100755 --- a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/NodeManager.java +++ b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/NodeManager.java @@ -27,6 +27,7 @@ import de.bwl.bwfla.emucomp.common.logging.PrefixLogger; import de.bwl.bwfla.emucomp.common.utils.ConfigHelpers; import de.bwl.bwfla.emucomp.components.AbstractEaasComponent; +import de.bwl.bwfla.emucomp.components.BindingsResolver; import de.bwl.bwfla.emucomp.components.emulators.EmulatorBean; import de.bwl.bwfla.emucomp.components.network.NetworkSwitchBean; import de.bwl.bwfla.emucomp.components.network.NodeTcpBean; @@ -102,6 +103,8 @@ public void init() throws BWFLAException { if (is == null) { throw new FileNotFoundException("Resource not found: " + componentDefaultConfigInitUri); } + BindingsResolver.providedConfigurationPath = componentDefaultConfigInitUri; + String data = new String(is.readAllBytes(), StandardCharsets.UTF_8); loadedComponentConfiguration.set(objectMapper.readValue(data, ComponentConfiguration.class)); diff --git a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/BindingsManager.java b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/BindingsManager.java index 51bf374..97c85b2 100755 --- a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/BindingsManager.java +++ b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/BindingsManager.java @@ -33,359 +33,344 @@ import java.util.logging.Logger; import java.util.stream.Stream; - -public class BindingsManager -{ - private final Logger log; - - private final Map bindings; - private final Map paths; - private final ImageMounter imageMounter; - - //TODO REMOVE MOCKED - private static final Object objectArchiveHelper = null; -// static { -// final var objectArchiveAddress = ConfigurationProvider.getConfiguration() -// .get("ws.objectarchive"); -// -// objectArchiveHelper = new ObjectArchiveHelper(objectArchiveAddress); -// } - - public enum EntryType - { - IMAGE("image"), - ALIAS("alias"), - FS_MOUNT("fsmnt"), - RAW_MOUNT("rawmnt"); - - private final String value; - - EntryType(String value) - { - this.value = value; - } - } - - private static final String ID_SEPARATOR = "/_____"; - - public BindingsManager() - { - this(Logger.getLogger(BindingsManager.class.getName())); - } - - public BindingsManager(Logger log) - { - this.bindings = new HashMap(); - this.paths = new LinkedHashMap(); - this.log = log; - this.imageMounter = new ImageMounter(log); - } - - /** Returns all registered bindings: binding's ID -> binding object */ - public Map entries() - { - return Collections.unmodifiableMap(bindings); - } - - /** Returns all mountpoints: binding's ID -> mountpoint */ - public Map mountpoints() - { - return Collections.unmodifiableMap(paths); - } - - /** Returns all mountpoints: binding's ID -> binding's access path */ - public Stream> paths() - { - return paths.entrySet().stream() - .filter((entry) -> !entry.getKey().contains(ID_SEPARATOR)); - } - - /** Returns a binding by its ID. */ - public Binding get(String id) - { - return bindings.get(id); - } - - /** Registers a new binding. */ - public void register(AbstractDataResource resource) throws BWFLAException - { - final String id = resource.getId(); - if (id == null || id.isEmpty()) - throw new IllegalArgumentException("Invalid resource's ID!"); - - if (resource instanceof Binding) { - this.put(id, (Binding) resource); - } - else if (resource instanceof ObjectArchiveBinding) { - // If the resource is an ArchiveBinding, query the archive - // and add all entries from the file collection - final ObjectArchiveBinding object = (ObjectArchiveBinding) resource; - final FileCollection fc = null; -// objectArchiveHelper.getObjectReference(object.getArchive(), object.getId()); - if (fc == null || fc.id == null || fc.id.isEmpty()) - throw new BWFLAException("Retrieving object meta data failed!"); - - for (FileCollectionEntry link : fc.files) { - if (link.getId() == null || link.getUrl() == null) - continue; - - this.put(id + "/" + link.getId(), link); - } - } - else { - final String clazz = resource.getClass().getName(); - throw new IllegalArgumentException("Unsupported resource type: " + clazz); - } - } - - /** Returns path to the binding's image, or null if not mounted or avalilable. */ - public String lookup(String binding) - { - final String prefix = "binding://"; - if (binding.startsWith(prefix)) - binding = binding.substring(prefix.length()); - - return paths.get(binding); - } - - /** Returns a collection of all binding-ids starting with prefix. */ - public Stream find(String prefix) - { - return bindings.keySet().stream() - .filter((id) -> id.startsWith(prefix)); - } - - private static void prepareResourceBinding(String componentId, Binding resource) throws IllegalArgumentException - { +import static de.bwl.bwfla.emucomp.components.BindingsResolver.findFileCollectionDeclaration; + + +public class BindingsManager { + private final Logger log; + + private final Map bindings; + private final Map paths; + private final ImageMounter imageMounter; + + public enum EntryType { + IMAGE("image"), + ALIAS("alias"), + FS_MOUNT("fsmnt"), + RAW_MOUNT("rawmnt"); + + private final String value; + + EntryType(String value) { + this.value = value; + } + } + + private static final String ID_SEPARATOR = "/_____"; + + public BindingsManager() { + this(Logger.getLogger(BindingsManager.class.getName())); + } + + public BindingsManager(Logger log) { + this.bindings = new HashMap(); + this.paths = new LinkedHashMap(); + this.log = log; + this.imageMounter = new ImageMounter(log); + } + + /** + * Returns all registered bindings: binding's ID -> binding object + */ + public Map entries() { + return Collections.unmodifiableMap(bindings); + } + + /** + * Returns all mountpoints: binding's ID -> mountpoint + */ + public Map mountpoints() { + return Collections.unmodifiableMap(paths); + } + + /** + * Returns all mountpoints: binding's ID -> binding's access path + */ + public Stream> paths() { + return paths.entrySet().stream() + .filter((entry) -> !entry.getKey().contains(ID_SEPARATOR)); + } + + /** + * Returns a binding by its ID. + */ + public Binding get(String id) { + return bindings.get(id); + } + + /** + * Registers a new binding. + */ + public void register(AbstractDataResource resource) throws BWFLAException { + final String id = resource.getId(); + if (id == null || id.isEmpty()) + throw new IllegalArgumentException("Invalid resource's ID!"); + + if (resource instanceof Binding) { + this.put(id, (Binding) resource); + } else if (resource instanceof ObjectArchiveBinding) { + // If the resource is an ArchiveBinding, query the archive + // and add all entries from the file collection + final ObjectArchiveBinding object = (ObjectArchiveBinding) resource; + final Optional fc = findFileCollectionDeclaration(object.getArchive(), object.getId()); + if (fc.isEmpty()) + throw new BWFLAException("Retrieving object meta data failed!"); + FileCollection collection = fc.get(); + if (collection.getId() == null || collection.getId().isEmpty()) { + throw new BWFLAException("Retrieving object meta data failed! - Object is not defined properly!\n" + collection); + } + for (FileCollectionEntry link : collection.files) { + if (link.getId() == null || link.getUrl() == null) + continue; + + this.put(id + "/" + link.getId(), link); + } + } else { + final String clazz = resource.getClass().getName(); + throw new IllegalArgumentException("Unsupported resource type: " + clazz); + } + } + + /** + * Returns path to the binding's image, or null if not mounted or avalilable. + */ + public String lookup(String binding) { + final String prefix = "binding://"; + if (binding.startsWith(prefix)) + binding = binding.substring(prefix.length()); + + return paths.get(binding); + } + + /** + * Returns a collection of all binding-ids starting with prefix. + */ + public Stream find(String prefix) { + return bindings.keySet().stream() + .filter((id) -> id.startsWith(prefix)); + } + + private static void prepareResourceBinding(String componentId, Binding resource) throws IllegalArgumentException { /* ImageArchive Bindings contain no valid URLs, just image IDs We delegate the resolving image IDs to the proxy. Other resources may contain valid URLs. */ - if (resource instanceof ImageArchiveBinding) - { - final var binding = (ImageArchiveBinding) resource; - final var location = DataResolvers.resolve(componentId, binding); - resource.setUrl(location); - } - - // Resolve object-archive's binding URLs! - if (resource instanceof FileCollectionEntry) - { - final var location = DataResolvers.objects() - .resolve(componentId, (FileCollectionEntry) resource); - - resource.setUrl(location); - } - - if (resource.getId() == null - || resource.getId().isEmpty() - || resource.getUrl() == null - || resource.getUrl().isEmpty()) - throw new IllegalArgumentException( - "Given resource is null, has invalid id or empty url."); - - if (resource.getAccess() == null) - resource.setAccess(Binding.AccessType.COW); - } - - /** - * Resolves and mounts a binding location of either the form - * binding://> or . - * - * The is replaced with the actual filesystem location of the binding's mountpoint. - * - * @param binding A binding location - * @return The resolved path or null, if the binding cannot be found - */ - public String mount(String componentId, String binding, Path outdir) - throws BWFLAException, IOException, IllegalArgumentException - { - if (binding == null || binding.isEmpty()) - throw new IllegalArgumentException("Binding is null or empty!"); - - if (binding.startsWith("rom://")) - binding = "rom-" + binding.substring("rom://".length()); - - if (binding.startsWith("binding://")) - binding = binding.substring("binding://".length()); - - // if (binding.contains("/")) - // throw new BWFLAException("Subresource bindings are currently not supported!"); - - // Let's see if we already have the resource mounted - String resourcePath = this.lookup(binding); - if (resourcePath != null) - return resourcePath; - - log.info("Resolving resource '" + binding + "'..."); - - final String realBindingId = binding; - final Binding resource = bindings.get(realBindingId); - if (resource == null) - throw new BWFLAException("Could not find binding for resource " + binding); - - log.info("Mounting binding '" + binding + "'..."); - - prepareResourceBinding(componentId, resource); - - // TODO: we need to resolve the full path here or earlier to - // ensure that all access options use the same path: - if(binding.startsWith("rom-")) // old rom bindings do not have COPY access by default - { - log.info("guessing resource is a ROM. force COPY access if not QCOW"); - - // check the file type first. - ImageInformation inf = new ImageInformation(resource.getUrl(), log); - ImageInformation.QemuImageFormat fmt = inf.getFileFormat(); - if(fmt != ImageInformation.QemuImageFormat.QCOW2) - resource.setAccess(Binding.AccessType.COPY); - } - - final MountOptions mountOpts = new MountOptions(); - if (resource.getFileSize() > 0) - mountOpts.setSize(resource.getFileSize()); - - Path imgPath = null; - ImageMounter.Mount mount = null; - switch (resource.getAccess()) { - case COW: - imgPath = outdir.resolve(realBindingId + ".cow"); - - QcowOptions qcowOptions = new QcowOptions(); - qcowOptions.setBackingFile(resource.getUrl()); - - EmulatorUtils.createCowFile(imgPath, qcowOptions); - - Path rawImagePath = outdir.resolve(realBindingId + ".dd"); - mount = imageMounter.mount(imgPath, rawImagePath, mountOpts); - - this.put(realBindingId, EntryType.RAW_MOUNT, resourcePath); - resourcePath = mount.getMountPoint().toAbsolutePath().toString(); - break; - case COPY: - imgPath = outdir.resolve(realBindingId + ".copy"); - EmulatorUtils.copyRemoteUrl(resource, imgPath, log); - resourcePath =imgPath.toString(); - break; - } - this.put(realBindingId, EntryType.IMAGE, imgPath.toAbsolutePath().toString()); - - // resourcePath is now the base path for the binding we want to find - if (resourcePath == null || !(new File(resourcePath).canRead())) { - final String message = "Binding target at location " - + resource.getUrl() + " cannot be accessed!"; - - throw new BWFLAException(message); - } - - // Is the raw file a block device with a filesystem? - final String fsType = this.getImageFileSystem(resource); - if (mount != null && fsType != null && !fsType.isEmpty()) { - final Path mountpoint = outdir.resolve(realBindingId + "." + fsType.replace(',', '.') + ".fuse"); - imageMounter.mount(mount, mountpoint, FileSystemType.fromString(fsType)); - resourcePath = mountpoint.toString(); - this.put(realBindingId, EntryType.FS_MOUNT, resourcePath); - } - - // Is local alias specified? - final String alias = resource.getLocalAlias(); - if (alias != null) { - final Path link = outdir.resolve(alias); - Files.deleteIfExists(link); - Files.createSymbolicLink(link, Paths.get(resourcePath)); - resourcePath = link.toString(); - - this.put(realBindingId, EntryType.ALIAS, resourcePath); - } - - this.add(realBindingId, resourcePath); - return resourcePath; - } - - /** Unmounts all registered bindings */ - public void cleanup() - { - log.info("Unmounting bindings..."); - { - final Set idsToRemove = new HashSet(); - - final String[] fuseIdSuffixes = new String[] { - ID_SEPARATOR + EntryType.RAW_MOUNT.value, - ID_SEPARATOR + EntryType.FS_MOUNT.value, - }; - - paths.forEach((id, path) -> { - for (String suffix : fuseIdSuffixes) { - if (!id.endsWith(suffix)) - continue; // Not a FUSE-mount! - - final int end = id.length() - suffix.length(); - idsToRemove.add(id.substring(0, end)); - } - }); - - imageMounter.unmount(); - idsToRemove.forEach((id) -> this.remove(id)); - idsToRemove.clear(); - } - } - - public static String toBindingId(String base, EntryType type) - { - return (base + ID_SEPARATOR + type.value); - } - - - /* =============== Internal Helpers =============== */ - - private void put(String id, Binding binding) - { - bindings.put(id, binding); - - log.info("Added binding: " + id); - } - - private void put(String base, EntryType type, String path) - { - final String id = BindingsManager.toBindingId(base, type); - paths.put(id, path); - - log.info("Added binding's path entry: " + id + " -> " + path); - } - - private void add(String id, String path) - { - paths.put(id, path); - - log.info("Added binding's access path: " + id + " -> " + path); - } - - private void remove(String id) - { - bindings.remove(id); - paths.remove(id); - for (EntryType type : EntryType.values()) - paths.remove(BindingsManager.toBindingId(id, type)); - } - - private String getImageFileSystem(Binding resource) - { - String fstype = null; - - if (resource instanceof ImageArchiveBinding) { - final ImageArchiveBinding image = (ImageArchiveBinding) resource; - fstype = image.getFileSystemType(); - } - else if (resource instanceof BlobStoreBinding) { - final BlobStoreBinding image = (BlobStoreBinding) resource; - if (image.getFileSystemType() != null && image.getMountFS()) - fstype = image.getFileSystemType().toString(); - } - - if (fstype != null) - fstype = fstype.toLowerCase(); - - return fstype; - } + if (resource instanceof ImageArchiveBinding) { + final var binding = (ImageArchiveBinding) resource; + final var location = DataResolvers.resolve(componentId, binding); + resource.setUrl(location); + } + + // Resolve object-archive's binding URLs! + if (resource instanceof FileCollectionEntry) { + final var location = DataResolvers.objects() + .resolve(componentId, (FileCollectionEntry) resource); + + resource.setUrl(location); + } + + if (resource.getId() == null + || resource.getId().isEmpty() + || resource.getUrl() == null + || resource.getUrl().isEmpty()) + throw new IllegalArgumentException( + "Given resource is null, has invalid id or empty url."); + + if (resource.getAccess() == null) + resource.setAccess(Binding.AccessType.COW); + } + + /** + * Resolves and mounts a binding location of either the form + * binding://> or . + *

+ * The is replaced with the actual filesystem location of the binding's mountpoint. + * + * @param binding A binding location + * @return The resolved path or null, if the binding cannot be found + */ + public String mount(String componentId, String binding, Path outdir) + throws BWFLAException, IOException, IllegalArgumentException { + if (binding == null || binding.isEmpty()) + throw new IllegalArgumentException("Binding is null or empty!"); + + if (binding.startsWith("rom://")) + binding = "rom-" + binding.substring("rom://".length()); + + if (binding.startsWith("binding://")) + binding = binding.substring("binding://".length()); + + // if (binding.contains("/")) + // throw new BWFLAException("Subresource bindings are currently not supported!"); + + // Let's see if we already have the resource mounted + String resourcePath = this.lookup(binding); + if (resourcePath != null) + return resourcePath; + + log.info("Resolving resource '" + binding + "'..."); + + final String realBindingId = binding; + final Binding resource = bindings.get(realBindingId); + if (resource == null) + throw new BWFLAException("Could not find binding for resource " + binding); + + log.info("Mounting binding '" + binding + "'..."); + + prepareResourceBinding(componentId, resource); + + // TODO: we need to resolve the full path here or earlier to + // ensure that all access options use the same path: + if (binding.startsWith("rom-")) // old rom bindings do not have COPY access by default + { + log.info("guessing resource is a ROM. force COPY access if not QCOW"); + + // check the file type first. + ImageInformation inf = new ImageInformation(resource.getUrl(), log); + ImageInformation.QemuImageFormat fmt = inf.getFileFormat(); + if (fmt != ImageInformation.QemuImageFormat.QCOW2) + resource.setAccess(Binding.AccessType.COPY); + } + + final MountOptions mountOpts = new MountOptions(); + if (resource.getFileSize() > 0) + mountOpts.setSize(resource.getFileSize()); + + Path imgPath = null; + ImageMounter.Mount mount = null; + switch (resource.getAccess()) { + case COW: + imgPath = outdir.resolve(realBindingId + ".cow"); + + QcowOptions qcowOptions = new QcowOptions(); + qcowOptions.setBackingFile(resource.getUrl()); + + EmulatorUtils.createCowFile(imgPath, qcowOptions); + + Path rawImagePath = outdir.resolve(realBindingId + ".dd"); + mount = imageMounter.mount(imgPath, rawImagePath, mountOpts); + + this.put(realBindingId, EntryType.RAW_MOUNT, resourcePath); + resourcePath = mount.getMountPoint().toAbsolutePath().toString(); + break; + case COPY: + imgPath = outdir.resolve(realBindingId + ".copy"); + EmulatorUtils.copyRemoteUrl(resource, imgPath, log); + resourcePath = imgPath.toString(); + break; + } + this.put(realBindingId, EntryType.IMAGE, imgPath.toAbsolutePath().toString()); + + // resourcePath is now the base path for the binding we want to find + if (resourcePath == null || !(new File(resourcePath).canRead())) { + final String message = "Binding target at location " + + resource.getUrl() + " cannot be accessed!"; + + throw new BWFLAException(message); + } + + // Is the raw file a block device with a filesystem? + final String fsType = this.getImageFileSystem(resource); + if (mount != null && fsType != null && !fsType.isEmpty()) { + final Path mountpoint = outdir.resolve(realBindingId + "." + fsType.replace(',', '.') + ".fuse"); + imageMounter.mount(mount, mountpoint, FileSystemType.fromString(fsType)); + resourcePath = mountpoint.toString(); + this.put(realBindingId, EntryType.FS_MOUNT, resourcePath); + } + + // Is local alias specified? + final String alias = resource.getLocalAlias(); + if (alias != null) { + final Path link = outdir.resolve(alias); + Files.deleteIfExists(link); + Files.createSymbolicLink(link, Paths.get(resourcePath)); + resourcePath = link.toString(); + + this.put(realBindingId, EntryType.ALIAS, resourcePath); + } + + this.add(realBindingId, resourcePath); + return resourcePath; + } + + /** + * Unmounts all registered bindings + */ + public void cleanup() { + log.info("Unmounting bindings..."); + { + final Set idsToRemove = new HashSet(); + + final String[] fuseIdSuffixes = new String[]{ + ID_SEPARATOR + EntryType.RAW_MOUNT.value, + ID_SEPARATOR + EntryType.FS_MOUNT.value, + }; + + paths.forEach((id, path) -> { + for (String suffix : fuseIdSuffixes) { + if (!id.endsWith(suffix)) + continue; // Not a FUSE-mount! + + final int end = id.length() - suffix.length(); + idsToRemove.add(id.substring(0, end)); + } + }); + + imageMounter.unmount(); + idsToRemove.forEach((id) -> this.remove(id)); + idsToRemove.clear(); + } + } + + public static String toBindingId(String base, EntryType type) { + return (base + ID_SEPARATOR + type.value); + } + + + /* =============== Internal Helpers =============== */ + + private void put(String id, Binding binding) { + bindings.put(id, binding); + + log.info("Added binding: " + id); + } + + private void put(String base, EntryType type, String path) { + final String id = BindingsManager.toBindingId(base, type); + paths.put(id, path); + + log.info("Added binding's path entry: " + id + " -> " + path); + } + + private void add(String id, String path) { + paths.put(id, path); + + log.info("Added binding's access path: " + id + " -> " + path); + } + + private void remove(String id) { + bindings.remove(id); + paths.remove(id); + for (EntryType type : EntryType.values()) + paths.remove(BindingsManager.toBindingId(id, type)); + } + + private String getImageFileSystem(Binding resource) { + String fstype = null; + + if (resource instanceof ImageArchiveBinding) { + final ImageArchiveBinding image = (ImageArchiveBinding) resource; + fstype = image.getFileSystemType(); + } else if (resource instanceof BlobStoreBinding) { + final BlobStoreBinding image = (BlobStoreBinding) resource; + if (image.getFileSystemType() != null && image.getMountFS()) + fstype = image.getFileSystemType().toString(); + } + + if (fstype != null) + fstype = fstype.toLowerCase(); + + return fstype; + } } diff --git a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/BindingsResolver.java b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/BindingsResolver.java new file mode 100644 index 0000000..33d228d --- /dev/null +++ b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/BindingsResolver.java @@ -0,0 +1,142 @@ +package de.bwl.bwfla.emucomp.components; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.bwl.bwfla.emucomp.common.ComponentConfiguration; +import de.bwl.bwfla.emucomp.common.FileCollection; +import de.bwl.bwfla.emucomp.common.ImageArchiveBinding; +import de.bwl.bwfla.emucomp.common.MachineConfiguration; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class BindingsResolver { + + private static final String _classpath_DEFAULT_CONFIGURATION_PATH = "/config"; + + public static volatile String providedConfigurationPath; + private static StringBuffer extractedConfigurationHolder; + private static ComponentConfiguration extractedComponentConfiguration; + + private static final ThreadLocal mapperThreadLocal = ThreadLocal.withInitial(ObjectMapper::new); + + public static Optional findFileCollectionDeclaration(String objectArchive, String id) { + tryExtractConfiguration(); + + if (extractedComponentConfiguration instanceof MachineConfiguration) { + MachineConfiguration configuration = ((MachineConfiguration) extractedComponentConfiguration); + + if (configuration.getAbstractDataResource() != null) { + Set collect = configuration.getAttachedFiles() + .stream() + .filter(e -> e.getId().equals(id)) + .filter(e -> e.getArchive().equals(objectArchive)) + .collect(Collectors.toSet()); + + return collect.stream().findFirst(); + } + } + return Optional.empty(); + } + + public static Optional findFirstImageArchiveBindingDeclaration() { + tryExtractConfiguration(); + + if (extractedComponentConfiguration instanceof MachineConfiguration) { + MachineConfiguration configuration = ((MachineConfiguration) extractedComponentConfiguration); + + if (configuration.getAbstractDataResource() != null) { + Set collect = configuration.getAbstractDataResource() + .stream() + .filter(e -> e instanceof ImageArchiveBinding) + .map(e -> (ImageArchiveBinding) e) + .collect(Collectors.toSet()); + if (collect.isEmpty()) { + log.info("Cannot find any ImageArchiveBinding configuration at {}", providedConfigurationPath); + return Optional.empty(); + } + + log.info("Found {} ImageArchiveBinding declarations, returning {}", collect.size(), collect.stream().findFirst().get().getImageId()); + return collect.stream().findFirst(); + } + } + return Optional.empty(); + } + + public static synchronized void tryExtractConfiguration() { + if (extractedConfigurationHolder == null && (providedConfigurationPath == null || providedConfigurationPath.isEmpty())) { + try { + URL configDirUrl = BindingsResolver.class.getResource(_classpath_DEFAULT_CONFIGURATION_PATH); + + if (configDirUrl == null) { + throw new IOException("Configuration directory not found on classpath: " + _classpath_DEFAULT_CONFIGURATION_PATH); + } + + URI configDirUri = configDirUrl.toURI(); + File configDir = new File(configDirUri); + + if (!configDir.exists() || !configDir.isDirectory()) { + throw new IOException("Resolved path is not a directory: " + configDir.getAbsolutePath()); + } + + File[] jsonFiles = configDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".json")); + + if (jsonFiles == null || jsonFiles.length == 0) { + throw new IOException("No JSON files found in directory: " + configDir.getAbsolutePath()); + } + + if (jsonFiles[0] != null) { + providedConfigurationPath = jsonFiles[0].getAbsolutePath(); + loadConfiguration(_classpath_DEFAULT_CONFIGURATION_PATH + jsonFiles[0].getName()); + } else { + throw new IOException("No suitable JSON file could be chosen from: " + configDir.getAbsolutePath()); + } + return; + } catch (URISyntaxException | IOException e) { + log.error("Failed to extract configuration from classpath directory: {}, " + e.getMessage(), _classpath_DEFAULT_CONFIGURATION_PATH); + } catch (Exception e) { + log.error("An error occurred during configuration processing: " + e.getMessage()); + } + } + + File file = new File(providedConfigurationPath); + if (file.isDirectory()) { + try { + File[] jsonFiles = file.listFiles((dir, name) -> name.toLowerCase().endsWith(".json")); + + if (jsonFiles == null || jsonFiles.length == 0) { + throw new IOException("No JSON files found in directory: " + file.getAbsolutePath()); + } + loadConfiguration(jsonFiles[0].getAbsolutePath()); + } catch (IOException e) { + log.error("Failed to extract configuration from classpath directory: {}, " + e.getMessage(), file.getAbsolutePath()); + } + } else { + loadConfiguration(providedConfigurationPath); + } + } + + private static void loadConfiguration(String path) { + try (InputStream is = BindingsResolver.class.getResourceAsStream(path)) { + if (is == null) { + throw new FileNotFoundException("Resource not found after selection: " + path); + } + String data = new String(is.readAllBytes(), StandardCharsets.UTF_8); + extractedConfigurationHolder = new StringBuffer(data); + + extractedComponentConfiguration = mapperThreadLocal.get().readValue(data, ComponentConfiguration.class); + } catch (IOException e) { + log.error("Cannot load configuration from {}", path); + } + } +} diff --git a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/emulators/EmulatorBean.java b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/emulators/EmulatorBean.java index 796c0c0..b0f0df3 100755 --- a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/emulators/EmulatorBean.java +++ b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/components/emulators/EmulatorBean.java @@ -21,7 +21,6 @@ import com.openslx.eaas.common.util.RuncStateInformation; -import de.bwl.bwfla.emucomp.components.api.EmulatorComponent; import de.bwl.bwfla.emucomp.common.*; import de.bwl.bwfla.emucomp.common.EmulatorUtils.XmountOutputFormat; import de.bwl.bwfla.emucomp.common.datatypes.EmuCompState; @@ -39,8 +38,10 @@ import de.bwl.bwfla.emucomp.common.services.guacplay.record.SessionRecorder; import de.bwl.bwfla.emucomp.common.utils.*; import de.bwl.bwfla.emucomp.components.BindingsManager; +import de.bwl.bwfla.emucomp.components.BindingsResolver; import de.bwl.bwfla.emucomp.components.EaasComponentBean; import de.bwl.bwfla.emucomp.components.Tail; +import de.bwl.bwfla.emucomp.components.api.EmulatorComponent; import de.bwl.bwfla.emucomp.components.emulators.IpcDefs.EventID; import de.bwl.bwfla.emucomp.components.emulators.IpcDefs.MessageType; import de.bwl.bwfla.emucomp.control.IPCWebsocketProxy; @@ -50,7 +51,6 @@ import de.bwl.bwfla.emucomp.xpra.IAudioStreamer; import de.bwl.bwfla.emucomp.xpra.PulseAudioStreamer; import org.apache.commons.io.FileUtils; - import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.glyptodon.guacamole.GuacamoleException; @@ -85,8 +85,7 @@ /** * @author iv1004 */ -public abstract class EmulatorBean extends EaasComponentBean implements EmulatorComponent -{ +public abstract class EmulatorBean extends EaasComponentBean implements EmulatorComponent { private EmulatorBeanMode emuBeanMode; @Inject @@ -97,872 +96,847 @@ public abstract class EmulatorBean extends EaasComponentBean implements Emulator @ConfigProperty(name = "emucomp.alsa_card") public String alsa_card; - @Inject - @ConfigProperty(name = "emucomp.libfaketime") - public String libfaketime; - - // allow beans to disable the fake clock preload - protected boolean disableFakeClock = false; - - private boolean isPulseAudioEnabled = false; - - @Inject - protected ThreadFactory workerThreadFactory; - - @Inject - @Named("managed-executor") - protected ExecutorService executor; - - @Inject - @ConfigProperty(name = "rest.blobstore") - protected String blobStoreRestAddress; + @Inject + @ConfigProperty(name = "emucomp.libfaketime") + public String libfaketime; - private final String containerOutput = "container-output"; + // allow beans to disable the fake clock preload + protected boolean disableFakeClock = false; - private static final String emulatorDataBase = "/home/bwfla/server-data/emulator-data/"; - - protected final TunnelConfig tunnelConfig = new TunnelConfig(); + private boolean isPulseAudioEnabled = false; - protected final EmulatorBeanState emuBeanState = new EmulatorBeanState(EmuCompState.EMULATOR_UNDEFINED, LOG); + @Inject + protected ThreadFactory workerThreadFactory; - protected MachineConfiguration emuEnvironment; - private String emuNativeConfig; - protected final Map containers = Collections.synchronizedMap(new HashMap()); + @Inject + @Named("managed-executor") + protected ExecutorService executor; - protected final ProcessRunner emuRunner = new ProcessRunner(); - protected final ArrayList vdeProcesses = new ArrayList(); + @Inject + @ConfigProperty(name = "rest.blobstore") + protected String blobStoreRestAddress; - protected final BindingsManager bindings = new BindingsManager(LOG); + private final String containerOutput = "container-output"; - protected String protocol; + private static final String emulatorDataBase = "/home/bwfla/server-data/emulator-data/"; - /** Emulator's configuration settings */ - protected final EmulatorConfig emuConfig = new EmulatorConfig(); + protected final TunnelConfig tunnelConfig = new TunnelConfig(); - /* IPC for control messages */ - private IpcSocket ctlSocket = null; - protected IpcMessageWriter ctlMsgWriter = null; - protected IpcMessageReader ctlMsgReader = null; - private IpcMessageQueue ctlMsgQueue = new IpcMessageQueue(); - private IpcEventSet ctlEvents = new IpcEventSet(); - protected String emuCtlSocketName = null; + protected final EmulatorBeanState emuBeanState = new EmulatorBeanState(EmuCompState.EMULATOR_UNDEFINED, LOG); - /** Is a client attached to the emulator? */ - private final AtomicBoolean isClientAttachedFlag = new AtomicBoolean(false); + protected MachineConfiguration emuEnvironment; + private String emuNativeConfig; + protected final Map containers = Collections.synchronizedMap(new HashMap()); - /* Session recording + replay members */ - private SessionRecorder recorder = null; - private SessionPlayerWrapper player = null; + protected final ProcessRunner emuRunner = new ProcessRunner(); + protected final ArrayList vdeProcesses = new ArrayList(); - /** Tool for capturing of screenshots. */ - private ScreenShooter scrshooter = null; - - final boolean isScreenshotEnabled = ConfigProvider.getConfig().getValue("emucomp.enable_screenshooter", Boolean.class); + protected final BindingsManager bindings = new BindingsManager(LOG); - protected PostScriptPrinter printer = null; + protected String protocol; - /** Internal chain of IGuacInterceptors. */ - private final GuacInterceptorChain interceptors = new GuacInterceptorChain(2); + /** + * Emulator's configuration settings + */ + protected final EmulatorConfig emuConfig = new EmulatorConfig(); - /** Number of unprocessed messages, before message-processors start to block. */ - private static final int MESSAGE_BUFFER_CAPACITY = 4096; + /* IPC for control messages */ + private IpcSocket ctlSocket = null; + protected IpcMessageWriter ctlMsgWriter = null; + protected IpcMessageReader ctlMsgReader = null; + private IpcMessageQueue ctlMsgQueue = new IpcMessageQueue(); + private IpcEventSet ctlEvents = new IpcEventSet(); + protected String emuCtlSocketName = null; - /** Filename for temporary trace-files. */ - private static final String TRACE_FILE = "session" + GuacDefs.TRACE_FILE_EXT; + /** + * Is a client attached to the emulator? + */ + private final AtomicBoolean isClientAttachedFlag = new AtomicBoolean(false); - /* Supported protocol names */ - private static final String PROTOCOL_SDLONP = "sdlonp"; - private static final String PROTOCOL_Y11 = "y11"; + /* Session recording + replay members */ + private SessionRecorder recorder = null; + private SessionPlayerWrapper player = null; - /* Supported audio driver names */ - private static final String AUDIODRIVER_PULSE = "pulse"; + /** + * Tool for capturing of screenshots. + */ + private ScreenShooter scrshooter = null; - /** Data directory inside of the emulator-containers */ - private static final String EMUCON_DATA_DIR = "/emucon/data"; + final boolean isScreenshotEnabled = ConfigProvider.getConfig().getValue("emucomp.enable_screenshooter", Boolean.class); - /** Binding ID for container's root filesystem */ - private static final String EMUCON_ROOTFS_BINDING_ID = "emucon-rootfs"; + protected PostScriptPrinter printer = null; - private static final String EMULATOR_DEFAULT_ARCHIVE = "emulators"; + /** + * Internal chain of IGuacInterceptors. + */ + private final GuacInterceptorChain interceptors = new GuacInterceptorChain(2); - @Inject - @ConfigProperty(name = "components.emulator_containers.enabled") - protected boolean emuContainerModeEnabled = true; + /** + * Number of unprocessed messages, before message-processors start to block. + */ + private static final int MESSAGE_BUFFER_CAPACITY = 4096; - @Inject - @ConfigProperty(name = "components.emulator_containers.uid") - protected String emuContainerUserId = null; + /** + * Filename for temporary trace-files. + */ + private static final String TRACE_FILE = "session" + GuacDefs.TRACE_FILE_EXT; - @Inject - @ConfigProperty(name = "components.emulator_containers.gid") - protected String emuContainerGroupId = null; + /* Supported protocol names */ + private static final String PROTOCOL_SDLONP = "sdlonp"; + private static final String PROTOCOL_Y11 = "y11"; - /** Files to include into a container-checkpoint */ - protected List emuContainerFilesToCheckpoint = new ArrayList<>(); + /* Supported audio driver names */ + private static final String AUDIODRIVER_PULSE = "pulse"; - /** File extension for checkpoints */ - private static final String CHECKPOINT_FILE_EXTENSION = ".tar.gz"; + /** + * Data directory inside of the emulator-containers + */ + private static final String EMUCON_DATA_DIR = "/emucon/data"; - protected boolean isKvmDeviceEnabled = false; + /** + * Binding ID for container's root filesystem + */ + private static final String EMUCON_ROOTFS_BINDING_ID = "emucon-rootfs"; + private static final String EMULATOR_DEFAULT_ARCHIVE = "emulators"; - public static EmulatorBean createEmulatorBean(MachineConfiguration env) throws ClassNotFoundException - { - String targetBean = env.getEmulator().getBean() + "Bean"; - Class beanClass = Class.forName(EmulatorBean.class.getPackage().getName() + "." + targetBean); - return (EmulatorBean)CDI.current().select(beanClass).get(); - } + @Inject + @ConfigProperty(name = "components.emulator_containers.enabled") + protected boolean emuContainerModeEnabled = true; - public boolean isClientAttached() - { - return isClientAttachedFlag.get(); - } + @Inject + @ConfigProperty(name = "components.emulator_containers.uid") + protected String emuContainerUserId = null; - public boolean isSdlBackendEnabled() - { - return (emuBeanMode == EmulatorBeanMode.SDLONP); - } + @Inject + @ConfigProperty(name = "components.emulator_containers.gid") + protected String emuContainerGroupId = null; + + /** + * Files to include into a container-checkpoint + */ + protected List emuContainerFilesToCheckpoint = new ArrayList<>(); + + /** + * File extension for checkpoints + */ + private static final String CHECKPOINT_FILE_EXTENSION = ".tar.gz"; + + protected boolean isKvmDeviceEnabled = false; + + + public static EmulatorBean createEmulatorBean(MachineConfiguration env) throws ClassNotFoundException { + String targetBean = env.getEmulator().getBean() + "Bean"; + Class beanClass = Class.forName(EmulatorBean.class.getPackage().getName() + "." + targetBean); + return (EmulatorBean) CDI.current().select(beanClass).get(); + } + + public boolean isClientAttached() { + return isClientAttachedFlag.get(); + } + + public boolean isSdlBackendEnabled() { + return (emuBeanMode == EmulatorBeanMode.SDLONP); + } + + public boolean isXpraBackendEnabled() { + return (emuBeanMode == EmulatorBeanMode.XPRA); + } + + public boolean isPulseAudioEnabled() { + return isPulseAudioEnabled; + } + + public boolean isLocalModeEnabled() { + return (emuBeanMode == EmulatorBeanMode.Y11); + } + + public boolean isHeadlessModeEnabled() { + return (emuBeanMode == EmulatorBeanMode.HEADLESS); + } + + public boolean isContainerModeEnabled() { + return emuContainerModeEnabled; + } + + public EmulatorBeanMode getEmuBeanMode() { + return emuBeanMode; + } + + public int getInactivityTimeout() { + return inactivityTimeout; + } + + @Override + public String getComponentType() { + return "machine"; + } + + boolean isHeadlessSupported() { + return false; + } + + boolean isBeanReady() { + return true; // this is the default, if the bean has no internal state + } + + @Override + public ComponentState getState() throws BWFLAException { + switch (this.getEmulatorState()) { + case EMULATOR_RUNNING: + return (this.isBeanReady()) ? ComponentState.RUNNING : ComponentState.INITIALIZING; + case EMULATOR_INACTIVE: + return ComponentState.INACTIVE; + case EMULATOR_STOPPED: + return ComponentState.STOPPED; + case EMULATOR_FAILED: + return ComponentState.FAILED; + default: + return ComponentState.INITIALIZING; + } + } + + public EmuCompState getEmulatorState() { + final boolean isEmulatorInactive = ctlEvents.poll(EventID.CLIENT_INACTIVE); + synchronized (emuBeanState) { + if (isEmulatorInactive) + emuBeanState.set(EmuCompState.EMULATOR_INACTIVE); + return emuBeanState.get(); + } + } + + public String getContainerId() { + final String compid = this.getComponentId(); + return compid.substring(1 + compid.lastIndexOf("+")); + } + + public String getContainerUserId() { + return emuContainerUserId; + } + + public String getContainerGroupId() { + return emuContainerGroupId; + } + + + public Function getContainerHostPathReplacer() { + final String hostDataDir = this.getDataDir().toString(); + final String hostBindingsDir = this.getBindingsDir().toString(); + return (cmdarg) -> { + return cmdarg.replaceAll(hostBindingsDir, EMUCON_DATA_DIR + "/bindings") + .replaceAll(hostDataDir, EMUCON_DATA_DIR); + }; + } + + public Path getDataDir() { + final Path workdir = this.getWorkingDir(); + if (this.isContainerModeEnabled()) + return workdir.resolve("data"); + + return workdir; + } + + public Path getNetworksDir() { + return this.getDataDir().resolve("networks"); + } + + public Path getSocketsDir() { + return this.getDataDir().resolve("sockets"); + } + + public Path getUploadsDir() { + return this.getDataDir().resolve("uploads"); + } + + public Path getStateDir() { + return this.getWorkingDir().resolve("state"); + } + + public Path getPrinterDir() { + return this.getDataDir().resolve("printer"); + } + + private void createWorkingSubDirs() throws IOException { + // Currently, working directory in container-mode is structured as follows: + // + // / + // state/ -> Container's memory dump + // data/ -> Session/emulator specific data + // networks/ -> Networking files + // sockets/ -> IO + CTRL sockets + // uploads/ -> Uploaded files + + // If container-mode is disabled: / == data/ + + Files.createDirectories(this.getDataDir()); + Files.createDirectories(this.getNetworksDir()); + Files.createDirectories(this.getSocketsDir()); + Files.createDirectories(this.getUploadsDir()); + Files.createDirectories(this.getPrinterDir()); + } + + private Path getXpraSocketPath() { + return this.getSocketsDir().resolve("xpra-iosocket"); + } + + private Path getPulseAudioSocketPath() { + return this.getSocketsDir().resolve("pulse-iosocket"); + } + + /** + * Returns emulator's runtime layer name. + */ + protected String getEmuContainerName(MachineConfiguration machineConfiguration) { + final String message = this.getClass().getSimpleName() + + " does not support container-mode!"; + + throw new UnsupportedOperationException(message); + } + + private String getEmulatorArchive() { + /* + Removed due to the migration to static resource declaration + */ +// String archive = ConfigProvider.getConfig().getValue("emucomp.emulator_archive", String.class); +// if(archive == null || archive.isEmpty()) +// return EMULATOR_DEFAULT_ARCHIVE; +// return archive; + + return EMULATOR_DEFAULT_ARCHIVE; + } + + public void initialize(ComponentConfiguration compConfig) throws BWFLAException { + synchronized (emuBeanState) { + final EmuCompState curstate = emuBeanState.get(); + if (curstate != EmuCompState.EMULATOR_UNDEFINED) { + String message = "Cannot initialize EmulatorBean!"; + throw new IllegalEmulatorStateException(message, curstate) + .setId(this.getComponentId()); + } - public boolean isXpraBackendEnabled() - { - return (emuBeanMode == EmulatorBeanMode.XPRA); - } + emuBeanState.set(EmuCompState.EMULATOR_BUSY); + } - public boolean isPulseAudioEnabled() - { - return isPulseAudioEnabled; - } - - public boolean isLocalModeEnabled() - { - return (emuBeanMode == EmulatorBeanMode.Y11); - } - - public boolean isHeadlessModeEnabled() { return (emuBeanMode == EmulatorBeanMode.HEADLESS); } - - public boolean isContainerModeEnabled() - { - return emuContainerModeEnabled; - } - - public EmulatorBeanMode getEmuBeanMode() - { - return emuBeanMode; - } - - public int getInactivityTimeout() - { - return inactivityTimeout; - } - - @Override - public String getComponentType() - { - return "machine"; - } - - boolean isHeadlessSupported() { return false; } - - boolean isBeanReady() { - return true; // this is the default, if the bean has no internal state - } - - @Override - public ComponentState getState() throws BWFLAException - { - switch (this.getEmulatorState()) { - case EMULATOR_RUNNING: - return (this.isBeanReady()) ? ComponentState.RUNNING : ComponentState.INITIALIZING; - case EMULATOR_INACTIVE: - return ComponentState.INACTIVE; - case EMULATOR_STOPPED: - return ComponentState.STOPPED; - case EMULATOR_FAILED: - return ComponentState.FAILED; - default: - return ComponentState.INITIALIZING; - } - } - - public EmuCompState getEmulatorState() - { - final boolean isEmulatorInactive = ctlEvents.poll(EventID.CLIENT_INACTIVE); - synchronized (emuBeanState) { - if (isEmulatorInactive) - emuBeanState.set(EmuCompState.EMULATOR_INACTIVE); - return emuBeanState.get(); - } - } - - public String getContainerId() - { - final String compid = this.getComponentId(); - return compid.substring(1 + compid.lastIndexOf("+")); - } - - public String getContainerUserId() - { - return emuContainerUserId; - } - - public String getContainerGroupId() - { - return emuContainerGroupId; - } - - - public Function getContainerHostPathReplacer() - { - final String hostDataDir = this.getDataDir().toString(); - final String hostBindingsDir = this.getBindingsDir().toString(); - return (cmdarg) -> { - return cmdarg.replaceAll(hostBindingsDir, EMUCON_DATA_DIR + "/bindings") - .replaceAll(hostDataDir, EMUCON_DATA_DIR); - }; - } - - public Path getDataDir() - { - final Path workdir = this.getWorkingDir(); - if (this.isContainerModeEnabled()) - return workdir.resolve("data"); - - return workdir; - } - - public Path getNetworksDir() - { - return this.getDataDir().resolve("networks"); - } - - public Path getSocketsDir() - { - return this.getDataDir().resolve("sockets"); - } - - public Path getUploadsDir() - { - return this.getDataDir().resolve("uploads"); - } - - public Path getStateDir() - { - return this.getWorkingDir().resolve("state"); - } - - public Path getPrinterDir() - { - return this.getDataDir().resolve("printer"); - } - - private void createWorkingSubDirs() throws IOException - { - // Currently, working directory in container-mode is structured as follows: - // - // / - // state/ -> Container's memory dump - // data/ -> Session/emulator specific data - // networks/ -> Networking files - // sockets/ -> IO + CTRL sockets - // uploads/ -> Uploaded files - - // If container-mode is disabled: / == data/ - - Files.createDirectories(this.getDataDir()); - Files.createDirectories(this.getNetworksDir()); - Files.createDirectories(this.getSocketsDir()); - Files.createDirectories(this.getUploadsDir()); - Files.createDirectories(this.getPrinterDir()); - } - - private Path getXpraSocketPath() - { - return this.getSocketsDir().resolve("xpra-iosocket"); - } - - private Path getPulseAudioSocketPath() - { - return this.getSocketsDir().resolve("pulse-iosocket"); - } - - /** Returns emulator's runtime layer name. */ - protected String getEmuContainerName(MachineConfiguration machineConfiguration) - { - final String message = this.getClass().getSimpleName() - + " does not support container-mode!"; - - throw new UnsupportedOperationException(message); - } - - private String getEmulatorArchive() { - String archive = ConfigProvider.getConfig().getValue("emucomp.emulator_archive", String.class); - if(archive == null || archive.isEmpty()) - return EMULATOR_DEFAULT_ARCHIVE; - return archive; - } - - public void initialize(ComponentConfiguration compConfig) throws BWFLAException - { - synchronized (emuBeanState) { - final EmuCompState curstate = emuBeanState.get(); - if (curstate != EmuCompState.EMULATOR_UNDEFINED) { - String message = "Cannot initialize EmulatorBean!"; - throw new IllegalEmulatorStateException(message, curstate) - .setId(this.getComponentId()); - } + final MachineConfiguration env = (MachineConfiguration) compConfig; + emuBeanMode = getEmuBeanMode(env); + emuRunner.setLogger(LOG); - emuBeanState.set(EmuCompState.EMULATOR_BUSY); - } - - final MachineConfiguration env = (MachineConfiguration) compConfig; - emuBeanMode = getEmuBeanMode(env); - emuRunner.setLogger(LOG); + try { + this.createWorkingSubDirs(); + } catch (IOException error) { + throw this.newInitFailureException("Creating working subdirs failed!", error); + } - try { - this.createWorkingSubDirs(); - } - catch (IOException error) { - throw this.newInitFailureException("Creating working subdirs failed!", error); - } + if (this.isSdlBackendEnabled()) { + // Create control sockets + try { + ctlSocket = IpcSocket.create(this.newCtlSocketName("srv"), IpcSocket.Type.DGRAM, true); + ctlMsgWriter = new IpcMessageWriter(ctlSocket); + ctlMsgReader = new IpcMessageReader(ctlSocket); + emuCtlSocketName = this.newCtlSocketName("emu"); + } catch (Throwable exception) { + throw this.newInitFailureException("Constructing control sockets failed!", exception); + } - if (this.isSdlBackendEnabled()) { - // Create control sockets - try { - ctlSocket = IpcSocket.create(this.newCtlSocketName("srv"), IpcSocket.Type.DGRAM, true); - ctlMsgWriter = new IpcMessageWriter(ctlSocket); - ctlMsgReader = new IpcMessageReader(ctlSocket); - emuCtlSocketName = this.newCtlSocketName("emu"); - } - catch (Throwable exception) { - throw this.newInitFailureException("Constructing control sockets failed!", exception); - } + // Prepare configuration for tunnels + tunnelConfig.setGuacdHostname("127.0.0.1"); + tunnelConfig.setGuacdPort(TunnelConfig.GUACD_PORT); + tunnelConfig.setInterceptor(interceptors); + } - // Prepare configuration for tunnels - tunnelConfig.setGuacdHostname("127.0.0.1"); - tunnelConfig.setGuacdPort(TunnelConfig.GUACD_PORT); - tunnelConfig.setInterceptor(interceptors); - } + emuConfig.setHardTermination(false); - emuConfig.setHardTermination(false); + try { + if (this.isContainerModeEnabled()) { + // Check, that rootfs-image is specified! + final boolean isRootFsFound = env.getAbstractDataResource().stream() + .anyMatch((resource) -> resource.getId().contentEquals(EMUCON_ROOTFS_BINDING_ID)); - try - { - if (this.isContainerModeEnabled()) { - // Check, that rootfs-image is specified! - final boolean isRootFsFound = env.getAbstractDataResource().stream() - .anyMatch((resource) -> resource.getId().contentEquals(EMUCON_ROOTFS_BINDING_ID)); - - if (!isRootFsFound) { - // Not found, try to get latest image ID as configured by the archive - final var image = this.findEmulatorImage(env); - env.getAbstractDataResource().add(image); - } - } + if (!isRootFsFound) { + // Not found, try to get latest image ID as configured by the archive + final var image = this.findEmulatorImage(); + env.getAbstractDataResource().add(image); + } + } - this.setRuntimeConfiguration(env); - } - catch (Throwable error) { - throw this.newInitFailureException("Initializing runtime configuration failed!", error); - } + this.setRuntimeConfiguration(env); + } catch (Throwable error) { + throw this.newInitFailureException("Initializing runtime configuration failed!", error); + } - LOG.info("Emulation session initialized in " + emuBeanMode.name() + " mode."); - LOG.info("Working directory created at: " + this.getWorkingDir()); - emuBeanState.update(EmuCompState.EMULATOR_READY); - } + LOG.info("Emulation session initialized in " + emuBeanMode.name() + " mode."); + LOG.info("Working directory created at: " + this.getWorkingDir()); + emuBeanState.update(EmuCompState.EMULATOR_READY); + } - private void unmountBindings() - { - bindings.cleanup(); - } + private void unmountBindings() { + bindings.cleanup(); + } - synchronized public void destroy() - { - synchronized (emuBeanState) - { - final EmuCompState curstate = emuBeanState.get(); - if (curstate == EmuCompState.EMULATOR_UNDEFINED) - return; + synchronized public void destroy() { + synchronized (emuBeanState) { + final EmuCompState curstate = emuBeanState.get(); + if (curstate == EmuCompState.EMULATOR_UNDEFINED) + return; - if (curstate == EmuCompState.EMULATOR_BUSY) { - LOG.severe("Destroying EmulatorBean while other operation is in-flight!"); - return; - } + if (curstate == EmuCompState.EMULATOR_BUSY) { + LOG.severe("Destroying EmulatorBean while other operation is in-flight!"); + return; + } - emuBeanState.set(EmuCompState.EMULATOR_UNDEFINED); - } - this.stopInternal(); + emuBeanState.set(EmuCompState.EMULATOR_UNDEFINED); + } + this.stopInternal(); - // free container IDs and remove corresp. files - for(File container: containers.values()) - container.delete(); + // free container IDs and remove corresp. files + for (File container : containers.values()) + container.delete(); - containers.clear(); + containers.clear(); - // kill vde networking threads - for(ProcessRunner subprocess : this.vdeProcesses) - { - if(subprocess.isProcessRunning()) - subprocess.stop(); + // kill vde networking threads + for (ProcessRunner subprocess : this.vdeProcesses) { + if (subprocess.isProcessRunning()) + subprocess.stop(); - subprocess.cleanup(); - } + subprocess.cleanup(); + } - this.unmountBindings(); + this.unmountBindings(); - // Stop screenshot-tool - if (scrshooter != null) - scrshooter.finish(); + // Stop screenshot-tool + if (scrshooter != null) + scrshooter.finish(); - // Stop and finalize session-recording - if (recorder != null && !recorder.isFinished()) - { - try { - recorder.finish(); - } - catch (IOException e) { - LOG.log(Level.SEVERE, e.getMessage(), e); - } - } + // Stop and finalize session-recording + if (recorder != null && !recorder.isFinished()) { + try { + recorder.finish(); + } catch (IOException e) { + LOG.log(Level.SEVERE, e.getMessage(), e); + } + } - // Cleanup the control sockets - try { - if (ctlSocket != null) - ctlSocket.close(); - } - catch (IOException exception) { - LOG.log(Level.SEVERE, exception.getMessage(), exception); - } + // Cleanup the control sockets + try { + if (ctlSocket != null) + ctlSocket.close(); + } catch (IOException exception) { + LOG.log(Level.SEVERE, exception.getMessage(), exception); + } - // Cleanup emulator's runner here - if (emuRunner.isProcessValid()) { - emuRunner.printStdOut(); - emuRunner.printStdErr(); - } + // Cleanup emulator's runner here + if (emuRunner.isProcessValid()) { + emuRunner.printStdOut(); + emuRunner.printStdErr(); + } - emuRunner.cleanup(); + emuRunner.cleanup(); - LOG.info("EmulatorBean destroyed."); + LOG.info("EmulatorBean destroyed."); - // Destroy base class! - super.destroy(); - } + // Destroy base class! + super.destroy(); + } - @Override - public void start() throws BWFLAException - { - synchronized (emuBeanState) { - final EmuCompState curstate = emuBeanState.get(); - if (curstate != EmuCompState.EMULATOR_READY && curstate != EmuCompState.EMULATOR_STOPPED) { - throw new BWFLAException("Cannot start emulator! Wrong state detected: " + curstate.value()) - .setId(this.getComponentId()); + @Override + public void start() throws BWFLAException { + synchronized (emuBeanState) { + final EmuCompState curstate = emuBeanState.get(); + if (curstate != EmuCompState.EMULATOR_READY && curstate != EmuCompState.EMULATOR_STOPPED) { + throw new BWFLAException("Cannot start emulator! Wrong state detected: " + curstate.value()) + .setId(this.getComponentId()); - } - emuBeanState.set(EmuCompState.EMULATOR_BUSY); - } + } + emuBeanState.set(EmuCompState.EMULATOR_BUSY); + } - try { - if (this.isSdlBackendEnabled() || this.isXpraBackendEnabled() || this.isHeadlessModeEnabled()) - this.startBackend(); - else { - throw new BWFLAException("Trying to start emulator using unimplemented mode: " + this.getEmuBeanMode()) - .setId(this.getComponentId()); - } - } - catch(Throwable error) { - emuBeanState.update(EmuCompState.EMULATOR_FAILED); - String message = "Starting emulator failed!"; - LOG.log(Level.SEVERE, message, error); - if (error.getMessage() != null) - message += " " + error.getMessage(); - - throw new BWFLAException(message, error) - .setId(this.getComponentId()); - } - } + try { + if (this.isSdlBackendEnabled() || this.isXpraBackendEnabled() || this.isHeadlessModeEnabled()) + this.startBackend(); + else { + throw new BWFLAException("Trying to start emulator using unimplemented mode: " + this.getEmuBeanMode()) + .setId(this.getComponentId()); + } + } catch (Throwable error) { + emuBeanState.update(EmuCompState.EMULATOR_FAILED); + String message = "Starting emulator failed!"; + LOG.log(Level.SEVERE, message, error); + if (error.getMessage() != null) + message += " " + error.getMessage(); + + throw new BWFLAException(message, error) + .setId(this.getComponentId()); + } + } - private void startBackend() throws BWFLAException, IOException - { - if (this.isLocalModeEnabled()) - LOG.info("Local-mode enabled. Emulator will be started locally!"); + private void startBackend() throws BWFLAException, IOException { + if (this.isLocalModeEnabled()) + LOG.info("Local-mode enabled. Emulator will be started locally!"); - if(!disableFakeClock) { - LOG.info("initializing fake clock"); - emuRunner.addEnvVariable("LD_PRELOAD", "/usr/local/lib/LD_PRELOAD_clock_gettime.so"); - } - if (this.isXpraBackendEnabled()) { - // TODO: implement this, if needed! - if (!this.isContainerModeEnabled()) { - throw new BWFLAException("Non-containerized XPRA sessions are not supported!") - .setId(this.getComponentId()); - } + if (!disableFakeClock) { + LOG.info("initializing fake clock"); + emuRunner.addEnvVariable("LD_PRELOAD", "/usr/local/lib/LD_PRELOAD_clock_gettime.so"); + } + if (this.isXpraBackendEnabled()) { + // TODO: implement this, if needed! + if (!this.isContainerModeEnabled()) { + throw new BWFLAException("Non-containerized XPRA sessions are not supported!") + .setId(this.getComponentId()); + } - final boolean isGpuEnabled = ConfigProvider.getConfig() - .getValue("components.xpra.enable_gpu", Boolean.class); + final boolean isGpuEnabled = ConfigProvider.getConfig() + .getValue("components.xpra.enable_gpu", Boolean.class); - if (isGpuEnabled) { - emuRunner.getCommand() - .add(0, "vglrun"); - } - } + if (isGpuEnabled) { + emuRunner.getCommand() + .add(0, "vglrun"); + } + } - String xprasockInContainer = null; + String xprasockInContainer = null; - if (this.isContainerModeEnabled()) { - LOG.info("Container-mode enabled. Emulator will be started inside of a container!"); + if (this.isContainerModeEnabled()) { + LOG.info("Container-mode enabled. Emulator will be started inside of a container!"); - final String cid = this.getContainerId(); - final String workdir = this.getWorkingDir().toString(); - final String rootfsdir = this.lookupResource(EMUCON_ROOTFS_BINDING_ID); + final String cid = this.getContainerId(); + final String workdir = this.getWorkingDir().toString(); + final String rootfsdir = this.lookupResource(EMUCON_ROOTFS_BINDING_ID); - // Generate container's config - { - final String conConfigPath = Paths.get(workdir, "config.json").toString(); - - final ProcessRunner cgen = new ProcessRunner(); - cgen.setCommand("emucon-cgen"); - cgen.addArguments("--output", conConfigPath); - cgen.addArguments("--user-id", emuContainerUserId); - cgen.addArguments("--group-id", emuContainerGroupId); - cgen.addArguments("--rootfs", rootfsdir); - - if (getEmulatorWorkdir() != null) { - cgen.addArguments("--workdir", getEmulatorWorkdir()); - } - - final String hostDataDir = this.getDataDir().toString(); - - final Function hostPathReplacer = this.getContainerHostPathReplacer(); - - // Mount emulator's data dir entries - try (Stream entries = Files.list(this.getDataDir())) { - entries.forEach((entry) -> { - final String path = entry.toString(); - cgen.addArgument("--mount"); - cgen.addArgument(path, ":", hostPathReplacer.apply(path), ":bind:rw"); - - if(path.contains("printer")) // compatibility for old snapshots - { - String target = hostPathReplacer.apply(path).replace("printer", "print"); - cgen.addArgument("--mount"); - cgen.addArgument(path, ":", target, ":bind:rw"); - } - }); - } - catch (Exception error) { - LOG.log(Level.WARNING, "Listing '" + hostDataDir + "' failed!\n", error); - emuBeanState.update(EmuCompState.EMULATOR_FAILED); - return; - } - - final List bindingIdsToSkip = new ArrayList<>(); - bindingIdsToSkip.add(EMUCON_ROOTFS_BINDING_ID); - if (emuEnvironment.hasCheckpointBindingId()) - bindingIdsToSkip.add(emuEnvironment.getCheckpointBindingId()); - - // Mount fuse-mounted bindings separately - bindings.paths().forEach((entry) -> { - final String curid = entry.getKey(); - for (String idToSkip : bindingIdsToSkip) { - if (curid.contentEquals(idToSkip)) - return; // Skip it! - } + // Generate container's config + { + final String conConfigPath = Paths.get(workdir, "config.json").toString(); - final String path = entry.getValue(); - cgen.addArgument("--mount"); - cgen.addArgument(path, ":", hostPathReplacer.apply(path), ":bind:rw"); - }); - - // Add emulator's env-vars with replaced host data directory - emuRunner.getEnvVariables() - .forEach((name, value) -> { - cgen.addArgument("--env"); - cgen.addArgument(name); - if (value != null && !value.isEmpty()) - cgen.addArgValues("=", hostPathReplacer.apply(value)); - }); - - // Enable KVM device (if needed) - if (isKvmDeviceEnabled) - cgen.addArgument("--enable-kvm"); - - final String conNetDir = hostPathReplacer.apply(this.getNetworksDir().toString()); - - // Add emulator's command with replaced host data directory - cgen.addArguments("--", "/usr/bin/emucon-init", "--networks-dir", conNetDir); - if (this.isXpraBackendEnabled()) { - final String xprasock = this.getXpraSocketPath().toString(); - xprasockInContainer = hostPathReplacer.apply(xprasock); - cgen.addArguments("--xpra-socket", xprasockInContainer); - } - - if (this.isPulseAudioEnabled()) { - final String pulsesock = this.getPulseAudioSocketPath().toString(); - cgen.addArguments("--pulse-socket", hostPathReplacer.apply(pulsesock)); - } - - cgen.addArgument("--"); - emuRunner.getCommand() - .forEach((cmdarg) -> cgen.addArgument(hostPathReplacer.apply(cmdarg))); - - cgen.setLogger(LOG); - if (!cgen.execute()) { - LOG.warning("Generating container's config failed!"); - emuBeanState.update(EmuCompState.EMULATOR_FAILED); - return; - } - - if (this.isSdlBackendEnabled()) { - // Replace host data directory in emulator's config - emuConfig.setIoSocket(hostPathReplacer.apply(emuConfig.getIoSocket())); - } - } + final ProcessRunner cgen = new ProcessRunner(); + cgen.setCommand("emucon-cgen"); + cgen.addArguments("--output", conConfigPath); + cgen.addArguments("--user-id", emuContainerUserId); + cgen.addArguments("--group-id", emuContainerGroupId); + cgen.addArguments("--rootfs", rootfsdir); - emuRunner.cleanup(); - - // Replace host's emulator command line... - emuRunner.setCommand("emucon-run"); - emuRunner.addArguments("--non-interactive"); - emuRunner.addArguments("--container-id", cid); - emuRunner.addArguments("--working-dir", workdir); - emuRunner.addArguments("--rootfs-type", "tree"); - emuRunner.addArguments("--rootfs-dir", rootfsdir); - if (emuEnvironment.hasCheckpointBindingId()) { - try { - final String checkpointBindingId = emuEnvironment.getCheckpointBindingId(); - final String checkpoint = this.lookupResource(checkpointBindingId); - emuRunner.addArguments("--checkpoint", checkpoint); - - LOG.info("Container state will be restored from checkpoint"); - } - catch (Exception error) { - throw new BWFLAException("Looking up checkpoint image failed!", error) - .setId(this.getComponentId()); - } - } - } + if (getEmulatorWorkdir() != null) { + cgen.addArguments("--workdir", getEmulatorWorkdir()); + } - emuRunner.redirectStdErrToStdOut(true); + final String hostDataDir = this.getDataDir().toString(); + + final Function hostPathReplacer = this.getContainerHostPathReplacer(); + + // Mount emulator's data dir entries + try (Stream entries = Files.list(this.getDataDir())) { + entries.forEach((entry) -> { + final String path = entry.toString(); + cgen.addArgument("--mount"); + cgen.addArgument(path, ":", hostPathReplacer.apply(path), ":bind:rw"); + + if (path.contains("printer")) // compatibility for old snapshots + { + String target = hostPathReplacer.apply(path).replace("printer", "print"); + cgen.addArgument("--mount"); + cgen.addArgument(path, ":", target, ":bind:rw"); + } + }); + } catch (Exception error) { + LOG.log(Level.WARNING, "Listing '" + hostDataDir + "' failed!\n", error); + emuBeanState.update(EmuCompState.EMULATOR_FAILED); + return; + } - if (!emuRunner.start()) { - throw new BWFLAException("Starting emulator failed!") - .setId(this.getComponentId()); - } + final List bindingIdsToSkip = new ArrayList<>(); + bindingIdsToSkip.add(EMUCON_ROOTFS_BINDING_ID); + if (emuEnvironment.hasCheckpointBindingId()) + bindingIdsToSkip.add(emuEnvironment.getCheckpointBindingId()); + + // Mount fuse-mounted bindings separately + bindings.paths().forEach((entry) -> { + final String curid = entry.getKey(); + for (String idToSkip : bindingIdsToSkip) { + if (curid.contentEquals(idToSkip)) + return; // Skip it! + } + + final String path = entry.getValue(); + cgen.addArgument("--mount"); + cgen.addArgument(path, ":", hostPathReplacer.apply(path), ":bind:rw"); + }); + + // Add emulator's env-vars with replaced host data directory + emuRunner.getEnvVariables() + .forEach((name, value) -> { + cgen.addArgument("--env"); + cgen.addArgument(name); + if (value != null && !value.isEmpty()) + cgen.addArgValues("=", hostPathReplacer.apply(value)); + }); + + // Enable KVM device (if needed) + if (isKvmDeviceEnabled) + cgen.addArgument("--enable-kvm"); + + final String conNetDir = hostPathReplacer.apply(this.getNetworksDir().toString()); + + // Add emulator's command with replaced host data directory + cgen.addArguments("--", "/usr/bin/emucon-init", "--networks-dir", conNetDir); + if (this.isXpraBackendEnabled()) { + final String xprasock = this.getXpraSocketPath().toString(); + xprasockInContainer = hostPathReplacer.apply(xprasock); + cgen.addArguments("--xpra-socket", xprasockInContainer); + } - if (this.isXpraBackendEnabled()) { - if (emuEnvironment.hasCheckpointBindingId()) { - this.waitUntilPathExists(this.getXpraSocketPath(), EmuCompState.EMULATOR_BUSY); - this.waitUntilRestoreDone(); - } - else { - final String rootfs = bindings.lookup(BindingsManager.toBindingId(EMUCON_ROOTFS_BINDING_ID, BindingsManager.EntryType.FS_MOUNT)); - final Path path = Paths.get(rootfs, "tmp", "xpra-started"); - this.waitUntilPathExists(path, EmuCompState.EMULATOR_BUSY); - this.waitUntilPathExists(this.getXpraSocketPath(), EmuCompState.EMULATOR_BUSY); - } - RuncStateInformation info = RuncStateInformation.getRuncStateInformationForComponent(this.getComponentId()); - IPCWebsocketProxy.wait(Path.of(xprasockInContainer), Path.of("/proc", info.getPid())); - } - else if (this.isSdlBackendEnabled()) { - if (emuEnvironment.hasCheckpointBindingId()) { - // Wait for socket re-creation after resuming from a checkpoint - this.waitUntilEmulatorCtlSocketAvailable(EmuCompState.EMULATOR_BUSY); - } - else { - // Perform the following steps only, when starting a new emulator! - // Skip them, when resuming from a checkpoint. + if (this.isPulseAudioEnabled()) { + final String pulsesock = this.getPulseAudioSocketPath().toString(); + cgen.addArguments("--pulse-socket", hostPathReplacer.apply(pulsesock)); + } - this.waitUntilEmulatorCtlSocketReady(EmuCompState.EMULATOR_BUSY); + cgen.addArgument("--"); + emuRunner.getCommand() + .forEach((cmdarg) -> cgen.addArgument(hostPathReplacer.apply(cmdarg))); - if (!this.sendEmulatorConfig()) - return; + cgen.setLogger(LOG); + if (!cgen.execute()) { + LOG.warning("Generating container's config failed!"); + emuBeanState.update(EmuCompState.EMULATOR_FAILED); + return; + } - if (!this.waitUntilEmulatorReady(EmuCompState.EMULATOR_BUSY)) - return; - } - } + if (this.isSdlBackendEnabled()) { + // Replace host data directory in emulator's config + emuConfig.setIoSocket(hostPathReplacer.apply(emuConfig.getIoSocket())); + } + } - if (this.isPulseAudioEnabled()) - this.waitUntilPathExists(this.getPulseAudioSocketPath(), EmuCompState.EMULATOR_BUSY); - - LOG.info("Emulator started in process " + emuRunner.getProcessId()); - - if (this.isSdlBackendEnabled()) { - final Thread ctlSockObserver = workerThreadFactory.newThread(() -> { - while (emuBeanState.fetch() != EmuCompState.EMULATOR_UNDEFINED) { - try { - // Try to receive new message - if (!ctlMsgReader.read(5000)) - continue; - - // Message could be read, queue it for further processing - if (ctlMsgReader.isNotification()) - ctlEvents.add(ctlMsgReader.getEventID()); - else { - final byte msgtype = ctlMsgReader.getMessageType(); - final byte[] msgdata = ctlMsgReader.getMessageData(); - ctlMsgQueue.put(msgtype, msgdata); - } - } - catch (Exception exception) { - if (emuBeanState.fetch() == EmuCompState.EMULATOR_UNDEFINED) - break; // Ignore problems when destroying session! + emuRunner.cleanup(); - LOG.warning("An error occured while reading from control-socket!"); - LOG.log(Level.SEVERE, exception.getMessage(), exception); - } - } - } - ); + // Replace host's emulator command line... + emuRunner.setCommand("emucon-run"); + emuRunner.addArguments("--non-interactive"); + emuRunner.addArguments("--container-id", cid); + emuRunner.addArguments("--working-dir", workdir); + emuRunner.addArguments("--rootfs-type", "tree"); + emuRunner.addArguments("--rootfs-dir", rootfsdir); + if (emuEnvironment.hasCheckpointBindingId()) { + try { + final String checkpointBindingId = emuEnvironment.getCheckpointBindingId(); + final String checkpoint = this.lookupResource(checkpointBindingId); + emuRunner.addArguments("--checkpoint", checkpoint); + + LOG.info("Container state will be restored from checkpoint"); + } catch (Exception error) { + throw new BWFLAException("Looking up checkpoint image failed!", error) + .setId(this.getComponentId()); + } + } + } - ctlSockObserver.start(); - } + emuRunner.redirectStdErrToStdOut(true); - final Thread emuObserver = workerThreadFactory.newThread(() -> { + if (!emuRunner.start()) { + throw new BWFLAException("Starting emulator failed!") + .setId(this.getComponentId()); + } - emuRunner.waitUntilFinished(); + if (this.isXpraBackendEnabled()) { + if (emuEnvironment.hasCheckpointBindingId()) { + this.waitUntilPathExists(this.getXpraSocketPath(), EmuCompState.EMULATOR_BUSY); + this.waitUntilRestoreDone(); + } else { + final String rootfs = bindings.lookup(BindingsManager.toBindingId(EMUCON_ROOTFS_BINDING_ID, BindingsManager.EntryType.FS_MOUNT)); + final Path path = Paths.get(rootfs, "tmp", "xpra-started"); + this.waitUntilPathExists(path, EmuCompState.EMULATOR_BUSY); + this.waitUntilPathExists(this.getXpraSocketPath(), EmuCompState.EMULATOR_BUSY); + } + RuncStateInformation info = RuncStateInformation.getRuncStateInformationForComponent(this.getComponentId()); + IPCWebsocketProxy.wait(Path.of(xprasockInContainer), Path.of("/proc", info.getPid())); + } else if (this.isSdlBackendEnabled()) { + if (emuEnvironment.hasCheckpointBindingId()) { + // Wait for socket re-creation after resuming from a checkpoint + this.waitUntilEmulatorCtlSocketAvailable(EmuCompState.EMULATOR_BUSY); + } else { + // Perform the following steps only, when starting a new emulator! + // Skip them, when resuming from a checkpoint. + + this.waitUntilEmulatorCtlSocketReady(EmuCompState.EMULATOR_BUSY); + + if (!this.sendEmulatorConfig()) + return; + + if (!this.waitUntilEmulatorReady(EmuCompState.EMULATOR_BUSY)) + return; + } + } - // upload emulator's output - if (this.isOutputAvailable()) { - LOG.info("Output is available!"); - try { - this.processEmulatorOutput(); - } - catch (BWFLAException error) { - LOG.log(Level.WARNING, "Processing emulator's output failed!", error); - } - } - else{ - LOG.info("No emulator output available!"); - } - - // cleanup will be performed later by EmulatorBean.destroy() - - synchronized (emuBeanState) { - if (EmulatorBean.this.isLocalModeEnabled()) { - // In local-mode emulator will be terminated by the user, - // without using our API. Set the correct state here! - emuBeanState.set(EmuCompState.EMULATOR_STOPPED); - } else { - final EmuCompState curstate = emuBeanState.get(); - if (curstate == EmuCompState.EMULATOR_RUNNING) { - LOG.warning("Emulator stopped unexpectedly!"); - // FIXME: setting here also to STOPPED, since there is currently no reliable way - // to determine (un-)successful termination depending on application exit code - emuBeanState.set(EmuCompState.EMULATOR_STOPPED); - } - } - } - }); + if (this.isPulseAudioEnabled()) + this.waitUntilPathExists(this.getPulseAudioSocketPath(), EmuCompState.EMULATOR_BUSY); + + LOG.info("Emulator started in process " + emuRunner.getProcessId()); + + if (this.isSdlBackendEnabled()) { + final Thread ctlSockObserver = workerThreadFactory.newThread(() -> { + while (emuBeanState.fetch() != EmuCompState.EMULATOR_UNDEFINED) { + try { + // Try to receive new message + if (!ctlMsgReader.read(5000)) + continue; + + // Message could be read, queue it for further processing + if (ctlMsgReader.isNotification()) + ctlEvents.add(ctlMsgReader.getEventID()); + else { + final byte msgtype = ctlMsgReader.getMessageType(); + final byte[] msgdata = ctlMsgReader.getMessageData(); + ctlMsgQueue.put(msgtype, msgdata); + } + } catch (Exception exception) { + if (emuBeanState.fetch() == EmuCompState.EMULATOR_UNDEFINED) + break; // Ignore problems when destroying session! + + LOG.warning("An error occured while reading from control-socket!"); + LOG.log(Level.SEVERE, exception.getMessage(), exception); + } + } + } + ); + + ctlSockObserver.start(); + } - emuObserver.start(); + final Thread emuObserver = workerThreadFactory.newThread(() -> { - if (printer != null) { - final Thread worker = workerThreadFactory.newThread(printer); - printer.setWorkerThread(worker); - worker.start(); - } + emuRunner.waitUntilFinished(); - if (this.isSdlBackendEnabled()) { - // Not in local mode? - if (!this.isLocalModeEnabled()) { - // Initialize the screenshot-tool - if (this.isScreenshotEnabled) { - scrshooter = new ScreenShooter(this.getComponentId(), 256); - scrshooter.prepare(); - - // Register the screenshot-tool - interceptors.addInterceptor(scrshooter); - } - } + // upload emulator's output + if (this.isOutputAvailable()) { + LOG.info("Output is available!"); + try { + this.processEmulatorOutput(); + } catch (BWFLAException error) { + LOG.log(Level.WARNING, "Processing emulator's output failed!", error); + } + } else { + LOG.info("No emulator output available!"); + } - // Prepare the connector for guacamole connections - { - final IThrowingSupplier clientTunnelCtor = () -> { - if (emuBeanState.fetch() == EmuCompState.EMULATOR_STOPPED) { - final var message = "Attaching client to stopped emulator failed!"; - LOG.warning(message); - throw new IllegalStateException(message); - } + // cleanup will be performed later by EmulatorBean.destroy() + + synchronized (emuBeanState) { + if (EmulatorBean.this.isLocalModeEnabled()) { + // In local-mode emulator will be terminated by the user, + // without using our API. Set the correct state here! + emuBeanState.set(EmuCompState.EMULATOR_STOPPED); + } else { + final EmuCompState curstate = emuBeanState.get(); + if (curstate == EmuCompState.EMULATOR_RUNNING) { + LOG.warning("Emulator stopped unexpectedly!"); + // FIXME: setting here also to STOPPED, since there is currently no reliable way + // to determine (un-)successful termination depending on application exit code + emuBeanState.set(EmuCompState.EMULATOR_STOPPED); + } + } + } + }); - final Runnable waitTask = () -> { - try { - EmulatorBean.this.attachClientToEmulator(); - EmulatorBean.this.waitForAttachedClient(); - } - catch (Exception exception) { - emuBeanState.update(EmuCompState.EMULATOR_FAILED); - LOG.log(Level.SEVERE, "Attaching client to emulator failed!", exception); - } - }; + emuObserver.start(); - executor.execute(waitTask); + if (printer != null) { + final Thread worker = workerThreadFactory.newThread(printer); + printer.setWorkerThread(worker); + worker.start(); + } - // Construct the tunnel - final GuacTunnel tunnel = GuacTunnel.construct(tunnelConfig); - if (!this.isLocalModeEnabled() && (player != null)) - player.start(tunnel, this.getComponentId(), emuRunner.getProcessMonitor()); + if (this.isSdlBackendEnabled()) { + // Not in local mode? + if (!this.isLocalModeEnabled()) { + // Initialize the screenshot-tool + if (this.isScreenshotEnabled) { + scrshooter = new ScreenShooter(this.getComponentId(), 256); + scrshooter.prepare(); - return (player != null) ? player.getPlayerTunnel() : tunnel; - }; + // Register the screenshot-tool + interceptors.addInterceptor(scrshooter); + } + } - this.addControlConnector(new GuacamoleConnector(clientTunnelCtor, emuConfig.isRelativeMouse())); - } - } - else if (this.isXpraBackendEnabled()) { - this.addControlConnector(new XpraConnector(this.getXpraSocketPath())); - } + // Prepare the connector for guacamole connections + { + final IThrowingSupplier clientTunnelCtor = () -> { + if (emuBeanState.fetch() == EmuCompState.EMULATOR_STOPPED) { + final var message = "Attaching client to stopped emulator failed!"; + LOG.warning(message); + throw new IllegalStateException(message); + } + + final Runnable waitTask = () -> { + try { + EmulatorBean.this.attachClientToEmulator(); + EmulatorBean.this.waitForAttachedClient(); + } catch (Exception exception) { + emuBeanState.update(EmuCompState.EMULATOR_FAILED); + LOG.log(Level.SEVERE, "Attaching client to emulator failed!", exception); + } + }; + + executor.execute(waitTask); + + // Construct the tunnel + final GuacTunnel tunnel = GuacTunnel.construct(tunnelConfig); + if (!this.isLocalModeEnabled() && (player != null)) + player.start(tunnel, this.getComponentId(), emuRunner.getProcessMonitor()); + + return (player != null) ? player.getPlayerTunnel() : tunnel; + }; + + this.addControlConnector(new GuacamoleConnector(clientTunnelCtor, emuConfig.isRelativeMouse())); + } + } else if (this.isXpraBackendEnabled()) { + this.addControlConnector(new XpraConnector(this.getXpraSocketPath())); + } - if (this.isPulseAudioEnabled()) { - final String cid = this.getComponentId(); - final Path pulsesock = this.getPulseAudioSocketPath(); - this.addControlConnector(new AudioConnector(() -> new PulseAudioStreamer(cid, pulsesock))); - } + if (this.isPulseAudioEnabled()) { + final String cid = this.getComponentId(); + final Path pulsesock = this.getPulseAudioSocketPath(); + this.addControlConnector(new AudioConnector(() -> new PulseAudioStreamer(cid, pulsesock))); + } - this.addControlConnector(new StdoutLogConnector(emuRunner.getStdOutPath())); - this.addControlConnector(new StderrLogConnector(emuRunner.getStdErrPath())); + this.addControlConnector(new StdoutLogConnector(emuRunner.getStdOutPath())); + this.addControlConnector(new StderrLogConnector(emuRunner.getStdErrPath())); - emuBeanState.update(EmuCompState.EMULATOR_RUNNING); - } + emuBeanState.update(EmuCompState.EMULATOR_RUNNING); + } - @Override - synchronized public String stop() throws BWFLAException - { - synchronized (emuBeanState) - { - final EmuCompState curstate = emuBeanState.get(); - if (curstate != EmuCompState.EMULATOR_RUNNING) { - LOG.warning("Cannot stop emulator! Wrong state detected: " + curstate.value()); - return this.getEmulatorOutputLocation(); - } + @Override + synchronized public String stop() throws BWFLAException { + synchronized (emuBeanState) { + final EmuCompState curstate = emuBeanState.get(); + if (curstate != EmuCompState.EMULATOR_RUNNING) { + LOG.warning("Cannot stop emulator! Wrong state detected: " + curstate.value()); + return this.getEmulatorOutputLocation(); + } - emuBeanState.set(EmuCompState.EMULATOR_BUSY); - } + emuBeanState.set(EmuCompState.EMULATOR_BUSY); + } - this.stopInternal(); + this.stopInternal(); - emuBeanState.update(EmuCompState.EMULATOR_STOPPED); - return this.getEmulatorOutputLocation(); - } + emuBeanState.update(EmuCompState.EMULATOR_STOPPED); + return this.getEmulatorOutputLocation(); + } - private boolean isOutputAvailable() - { - boolean hasOutput = (emuEnvironment.getOutputBindingId() != null && !emuEnvironment.getOutputBindingId().isEmpty()); - return hasOutput; - } + private boolean isOutputAvailable() { + boolean hasOutput = (emuEnvironment.getOutputBindingId() != null && !emuEnvironment.getOutputBindingId().isEmpty()); + return hasOutput; + } - //TODO when should this be used, when getChanged files? - private Path getAllFiles(BlobStoreBinding binding, Path cow, FileSystemType fsType) throws BWFLAException { - try (final ImageMounter mounter = new ImageMounter(LOG)) { - Path output; - if( binding.getResourceType() != Binding.ResourceType.ISO) { + //TODO when should this be used, when getChanged files? + private Path getAllFiles(BlobStoreBinding binding, Path cow, FileSystemType fsType) throws BWFLAException { + try (final ImageMounter mounter = new ImageMounter(LOG)) { + Path output; + if (binding.getResourceType() != Binding.ResourceType.ISO) { final Path workdir = this.getWorkingDir().resolve("output"); try { Files.createDirectories(workdir); - } - catch (IOException e) - { + } catch (IOException e) { throw new BWFLAException("failed creating workdir").setId(this.getComponentId()); } @@ -984,18 +958,17 @@ private Path getAllFiles(BlobStoreBinding binding, Path cow, FileSystemType fsTy Zip32Utils.zip(output.toFile(), srcdir.toFile(), exclude); } return output; + } else { + throw new BWFLAException("Unsupported binding type").setId(this.getComponentId()); } - else { - throw new BWFLAException("Unsupported binding type").setId(this.getComponentId()); - } - } - } + } + } - private Path getChangedFiles(Path cowImage) throws BWFLAException { - List partitionFiles = new ArrayList<>(); - QcowOptions qcowOptions = new QcowOptions(); - try (final ImageMounter mounter = new ImageMounter(LOG)) { - ImageInformation imageInformation = new ImageInformation(cowImage.toString(), LOG); + private Path getChangedFiles(Path cowImage) throws BWFLAException { + List partitionFiles = new ArrayList<>(); + QcowOptions qcowOptions = new QcowOptions(); + try (final ImageMounter mounter = new ImageMounter(LOG)) { + ImageInformation imageInformation = new ImageInformation(cowImage.toString(), LOG); String backingFileId = imageInformation.getBackingFile(); qcowOptions.setBackingFile(backingFileId); @@ -1009,123 +982,113 @@ private Path getChangedFiles(Path cowImage) throws BWFLAException { ImageMounter.Mount rawmnt = mounter.mount(lowerImgPath, workdir.resolve("lowerImageLayer.dd")); ImageMounter.Mount rawmnt2 = mounter.mount(cowImage, workdir.resolve("upperDir.dd")); - // load partition table - final DiskDescription disk = DiskDescription.read(Path.of(rawmnt.getTargetImage()), LOG); - if (!disk.hasPartitions()) - throw new BWFLAException("Disk seems to be not partitioned!"); + // load partition table + final DiskDescription disk = DiskDescription.read(Path.of(rawmnt.getTargetImage()), LOG); + if (!disk.hasPartitions()) + throw new BWFLAException("Disk seems to be not partitioned!"); - LOG.info("separating data by partition"); - for (DiskDescription.Partition partition : disk.getPartitions()) { - if (!partition.hasFileSystemType()) { - LOG.info("Partition " + partition.getIndex() + " is not formatted, skip"); - continue; - } - rawmnt = mounter.remount(rawmnt, partition.getStartOffset(), partition.getSize()); - rawmnt2 = mounter.remount(rawmnt2, partition.getStartOffset(), partition.getSize()); - - FileSystemType fsType; - try { - fsType = FileSystemType.fromString(partition.getFileSystemType()); - } - catch (IllegalArgumentException e) - { - LOG.warning("filesystem " + partition.getFileSystemType() + " not yet support. please report."); - continue; - } + LOG.info("separating data by partition"); + for (DiskDescription.Partition partition : disk.getPartitions()) { + if (!partition.hasFileSystemType()) { + LOG.info("Partition " + partition.getIndex() + " is not formatted, skip"); + continue; + } + rawmnt = mounter.remount(rawmnt, partition.getStartOffset(), partition.getSize()); + rawmnt2 = mounter.remount(rawmnt2, partition.getStartOffset(), partition.getSize()); - final ImageMounter.Mount fsmnt = mounter.mount(rawmnt, workdir.resolve("backingFileId.fs"), fsType); - final Path lowerDir = fsmnt.getMountPoint(); + FileSystemType fsType; + try { + fsType = FileSystemType.fromString(partition.getFileSystemType()); + } catch (IllegalArgumentException e) { + LOG.warning("filesystem " + partition.getFileSystemType() + " not yet support. please report."); + continue; + } - final ImageMounter.Mount fsmnt2 = mounter.mount(rawmnt2, workdir.resolve("upperDir.fs"), fsType); - final Path upperDir = fsmnt2.getMountPoint(); + final ImageMounter.Mount fsmnt = mounter.mount(rawmnt, workdir.resolve("backingFileId.fs"), fsType); + final Path lowerDir = fsmnt.getMountPoint(); - final Path outputDir = this.getWorkingDir().resolve("partition-" + partition.getIndex()); + final ImageMounter.Mount fsmnt2 = mounter.mount(rawmnt2, workdir.resolve("upperDir.fs"), fsType); + final Path upperDir = fsmnt2.getMountPoint(); - LOG.info("Executing RSYNC to determine changed files..."); - ProcessRunner processRunner = new ProcessRunner("rsync"); - processRunner.addArguments("-armxv"); //, "--progress"); when using --progress, rsync sometimes hangs... - processRunner.addArguments("--exclude", "dev"); - processRunner.addArguments("--exclude", "proc"); - processRunner.addArguments("--compare-dest=" + lowerDir.toAbsolutePath().toString() + "/", - upperDir.toAbsolutePath().toString() + "/", - outputDir.toAbsolutePath().toString()); - processRunner.execute(true); - processRunner.cleanup(); + final Path outputDir = this.getWorkingDir().resolve("partition-" + partition.getIndex()); - LOG.info("Done with rsync!"); + LOG.info("Executing RSYNC to determine changed files..."); + ProcessRunner processRunner = new ProcessRunner("rsync"); + processRunner.addArguments("-armxv"); //, "--progress"); when using --progress, rsync sometimes hangs... + processRunner.addArguments("--exclude", "dev"); + processRunner.addArguments("--exclude", "proc"); + processRunner.addArguments("--compare-dest=" + lowerDir.toAbsolutePath().toString() + "/", + upperDir.toAbsolutePath().toString() + "/", + outputDir.toAbsolutePath().toString()); + processRunner.execute(true); + processRunner.cleanup(); + LOG.info("Done with rsync!"); - partitionFiles.add(outputDir); - ProcessRunner cleaner = new ProcessRunner("find"); - cleaner.addArguments(outputDir.toAbsolutePath().toString(), "-empty", "-delete"); - cleaner.execute(true); - cleaner.cleanup(); - } + partitionFiles.add(outputDir); - Path outputTar = workdir.resolve("output.tgz"); - ProcessRunner tarCollectProc = new ProcessRunner("tar"); - tarCollectProc.addArguments("-czf"); - tarCollectProc.addArguments(outputTar.toAbsolutePath().toString()); - tarCollectProc.addArguments("-C", this.getWorkingDir().toAbsolutePath().toString()); - partitionFiles.forEach(p -> tarCollectProc.addArguments(p.getFileName().toString())); - tarCollectProc.execute(true); - tarCollectProc.cleanup(); + ProcessRunner cleaner = new ProcessRunner("find"); + cleaner.addArguments(outputDir.toAbsolutePath().toString(), "-empty", "-delete"); + cleaner.execute(true); + cleaner.cleanup(); + } - return outputTar; - } - catch (BWFLAException cause) { - cause.printStackTrace(); - throw cause; - } - catch (IOException ioException) - { + Path outputTar = workdir.resolve("output.tgz"); + ProcessRunner tarCollectProc = new ProcessRunner("tar"); + tarCollectProc.addArguments("-czf"); + tarCollectProc.addArguments(outputTar.toAbsolutePath().toString()); + tarCollectProc.addArguments("-C", this.getWorkingDir().toAbsolutePath().toString()); + partitionFiles.forEach(p -> tarCollectProc.addArguments(p.getFileName().toString())); + tarCollectProc.execute(true); + tarCollectProc.cleanup(); + + return outputTar; + } catch (BWFLAException cause) { + cause.printStackTrace(); + throw cause; + } catch (IOException ioException) { ioException.printStackTrace(); final String message = "IO Exception in getChangedFiles"; - throw new BWFLAException(message, ioException) - .setId(this.getComponentId()); + throw new BWFLAException(message, ioException) + .setId(this.getComponentId()); } - } - + } - private void processEmulatorOutput() throws BWFLAException - { - LOG.info("Processing emulator output ..."); - final String bindingId = emuEnvironment.getOutputBindingId(); - Path outputTar = null; + private void processEmulatorOutput() throws BWFLAException { + LOG.info("Processing emulator output ..."); - Binding b = bindings.get(bindingId); - if(b == null) { - final String message = "Unknown output bindingId " + bindingId; - final BWFLAException error = new BWFLAException(message) - .setId(this.getComponentId()); + final String bindingId = emuEnvironment.getOutputBindingId(); + Path outputTar = null; - this.result.completeExceptionally(error); - } + Binding b = bindings.get(bindingId); + if (b == null) { + final String message = "Unknown output bindingId " + bindingId; + final BWFLAException error = new BWFLAException(message) + .setId(this.getComponentId()); - try { + this.result.completeExceptionally(error); + } - if(emuEnvironment.isLinuxRuntime()){ + try { - for (AbstractDataResource r : emuEnvironment.getAbstractDataResource()) - { - if(r.getId() != null && r.getId().equals("rootfs")) - { - LOG.info("Linux runtime recognized, processing with rootfs..."); - final String cowImage = bindings.lookup(BindingsManager.toBindingId("rootfs", BindingsManager.EntryType.IMAGE)); - this.unmountBindings(); - outputTar = getChangedFiles(Path.of(cowImage)); + if (emuEnvironment.isLinuxRuntime()) { - } - } - } + for (AbstractDataResource r : emuEnvironment.getAbstractDataResource()) { + if (r.getId() != null && r.getId().equals("rootfs")) { + LOG.info("Linux runtime recognized, processing with rootfs..."); + final String cowImage = bindings.lookup(BindingsManager.toBindingId("rootfs", BindingsManager.EntryType.IMAGE)); + this.unmountBindings(); + outputTar = getChangedFiles(Path.of(cowImage)); - else { - final String qcow = bindings.lookup(BindingsManager.toBindingId(bindingId, BindingsManager.EntryType.IMAGE)); - this.unmountBindings(); - outputTar = getChangedFiles(Path.of(qcow)); - } + } + } + } else { + final String qcow = bindings.lookup(BindingsManager.toBindingId(bindingId, BindingsManager.EntryType.IMAGE)); + this.unmountBindings(); + outputTar = getChangedFiles(Path.of(qcow)); + } // final BlobDescription blob = new BlobDescription() // .setDescription("Output for session " + this.getComponentId()) @@ -1135,263 +1098,246 @@ private void processEmulatorOutput() throws BWFLAException // blob.setDataFromFile(outputTar); // blob.setType(".tgz"); - //TODO REPLACE WITH CLIENT CALL - // Upload archive to the BlobStore - BlobHandle handle = new BlobHandle(); + //TODO REPLACE WITH CLIENT CALL + // Upload archive to the BlobStore + BlobHandle handle = new BlobHandle(); if (handle == null) { throw new BWFLAException("Output result is null").setId(this.getComponentId()); - } - this.result.complete(handle); - } - catch (BWFLAException cause) { - final String message = "Creation of output.zip failed!"; - final BWFLAException error = new BWFLAException(message, cause) - .setId(this.getComponentId()); + } + this.result.complete(handle); + } catch (BWFLAException cause) { + final String message = "Creation of output.zip failed!"; + final BWFLAException error = new BWFLAException(message, cause) + .setId(this.getComponentId()); + + this.result.completeExceptionally(error); + throw error; + } + } + + private String getEmulatorOutputLocation() throws BWFLAException { + if (!this.isOutputAvailable()) + return null; + + try { + final BlobHandle handle = super.result.get(2, TimeUnit.MINUTES); + if (blobStoreRestAddress.contains("http://eaas:8080")) + return handle.toRestUrl(blobStoreRestAddress.replace("http://eaas:8080", "")); + else + return handle.toRestUrl(blobStoreRestAddress); + } catch (Exception error) { + final String message = "Waiting for emulator's output failed!"; + LOG.log(Level.WARNING, message, error); + throw new BWFLAException(message, error) + .setId(this.getComponentId()); + } + } - this.result.completeExceptionally(error); - throw error; - } - } - - private String getEmulatorOutputLocation() throws BWFLAException - { - if (!this.isOutputAvailable()) - return null; - - try { - final BlobHandle handle = super.result.get(2, TimeUnit.MINUTES); - if (blobStoreRestAddress.contains("http://eaas:8080")) - return handle.toRestUrl(blobStoreRestAddress.replace("http://eaas:8080", "")); - else - return handle.toRestUrl(blobStoreRestAddress); - } - catch (Exception error) { - final String message = "Waiting for emulator's output failed!"; - LOG.log(Level.WARNING, message, error); - throw new BWFLAException(message, error) - .setId(this.getComponentId()); - } - } - - void stopInternal() - { - if (player != null) - player.stop(); - - this.closeAllConnectors(); - - if (emuRunner.isProcessRunning()) - this.stopProcessRunner(emuRunner); - - if (printer != null) - printer.stop(); - - final var stdoutCon = (LogConnector) this.getControlConnector(StdoutLogConnector.PROTOCOL); - if(stdoutCon != null) - stdoutCon.cleanup(); - - final var stderrCon = (LogConnector) this.getControlConnector(StderrLogConnector.PROTOCOL); - if(stderrCon != null) - stderrCon.cleanup(); - - } - - private void closeAllConnectors() - { - if (this.isSdlBackendEnabled()) { - final GuacamoleConnector connector = (GuacamoleConnector) this.getControlConnector(GuacamoleConnector.PROTOCOL); - final GuacTunnel tunnel = (connector != null) ? connector.getTunnel() : null; - if (tunnel != null && tunnel.isOpen()) { - try { - tunnel.disconnect(); - tunnel.close(); - } - catch (GuacamoleException error) { - LOG.log(Level.SEVERE, "Closing Guacamole connector failed!", error); - } - } - } - else if (this.isXpraBackendEnabled()) { - final XpraConnector connector = (XpraConnector) this.getControlConnector(XpraConnector.PROTOCOL); - if (connector != null) { - try { - connector.disconnect(); - } - catch (Exception error) { - LOG.log(Level.SEVERE, "Closing Xpra connector failed!", error); - } - } - } + void stopInternal() { + if (player != null) + player.stop(); - if (this.isPulseAudioEnabled()) { - final AudioConnector connector = (AudioConnector) this.getControlConnector(AudioConnector.PROTOCOL); - final IAudioStreamer streamer = (connector != null) ? connector.getAudioStreamer() : null; - if (streamer != null && !streamer.isClosed()) { - try { - streamer.stop(); - streamer.close(); - } - catch (Exception error) { - LOG.log(Level.WARNING, "Stopping audio streamer failed!", error); - } - } - } - } - - void stopProcessRunner(ProcessRunner runner) - { - final int emuProcessId = runner.getProcessId(); - final var emuContainerId = this.getContainerId(); - if (this.isContainerModeEnabled()) - LOG.info("Stopping emulator " + emuProcessId + " in container " + emuContainerId + "..."); - else LOG.info("Stopping emulator " + emuProcessId + "..."); - - try { - if (this.isSdlBackendEnabled()) { - // Send termination message - ctlMsgWriter.begin(MessageType.TERMINATE); - ctlMsgWriter.send(emuCtlSocketName); - - // Give emulator a chance to shutdown cleanly - if (runner.waitUntilFinished(5, TimeUnit.SECONDS)) { - LOG.info("Emulator " + emuProcessId + " stopped."); - return; - } - } - } - catch (Exception exception) { - LOG.log(Level.SEVERE, "Stopping emulator failed!", exception); - } + this.closeAllConnectors(); - if (this.isContainerModeEnabled()) { - final var killer = new ProcessRunner(); - final var cmds = new ArrayList>(2); - cmds.add(List.of("runc", "kill", emuContainerId, "TERM")); - cmds.add(List.of("runc", "kill", "-a", emuContainerId, "KILL")); - for (final var args : cmds) { - killer.setCommand("sudo"); - killer.addArguments(args); - killer.setLogger(LOG); - killer.execute(); - - if (runner.waitUntilFinished(5, TimeUnit.SECONDS)) { - LOG.info("Emulator " + emuProcessId + " stopped."); - return; - } - } - } + if (emuRunner.isProcessRunning()) + this.stopProcessRunner(emuRunner); - LOG.warning("Emulator " + emuProcessId + " failed to shutdown cleanly! Killing it..."); - runner.stop(5, TimeUnit.SECONDS); // Try to terminate the process - runner.kill(); // Try to kill the process - } + if (printer != null) + printer.stop(); - @Override - public String getRuntimeConfiguration() throws BWFLAException - { - synchronized (emuBeanState) - { - if (emuBeanState.get() == EmuCompState.EMULATOR_UNDEFINED) { - String message = "Runtime configuration is not available in this state!"; - throw new IllegalEmulatorStateException(message, EmuCompState.EMULATOR_UNDEFINED) - .setId(this.getComponentId()); - } - } - try { - return this.emuEnvironment.value(); - } catch (JAXBException e) { - throw new BWFLAException("Serializing environment description failed!", e) - .setId(this.getComponentId()); - } - } - - @Override - public Set getColdplugableDrives() - { - // TODO: here read result from corresponding metadata - return new HashSet(); - } - - @Override - public Set getHotplugableDrives() - { - // TODO: here read result from corresponding metadata - return new HashSet(); - } - - private void sync() throws BWFLAException - { - final ProcessRunner process = new ProcessRunner(); - process.setCommand("sync"); - if (!process.execute()) { - throw new BWFLAException("Syncing filesystem failed!") - .setId(this.getComponentId()); - } + final var stdoutCon = (LogConnector) this.getControlConnector(StdoutLogConnector.PROTOCOL); + if (stdoutCon != null) + stdoutCon.cleanup(); - LOG.info("filesystem synced"); - } + final var stderrCon = (LogConnector) this.getControlConnector(StderrLogConnector.PROTOCOL); + if (stderrCon != null) + stderrCon.cleanup(); - @Override - public List snapshot() throws BWFLAException - { - synchronized (emuBeanState) { - final EmuCompState curstate = emuBeanState.get(); - if (curstate != EmuCompState.EMULATOR_STOPPED) { - String message = "Cannot save environment in this state!"; - throw new IllegalEmulatorStateException(message, curstate) - .setId(this.getComponentId()); - } - } + } - // Collect all modified images for used/mounted bindings - final Map images = new LinkedHashMap(); - for (AbstractDataResource resource : emuEnvironment.getAbstractDataResource()) { - if (!(resource instanceof Binding)) - continue; - - final Binding binding = (Binding) resource; - if(((Binding) resource).getAccess() == Binding.AccessType.COPY) - continue; - - final String id = binding.getId(); - - final String path = bindings.lookup(BindingsManager.toBindingId(id, BindingsManager.EntryType.IMAGE)); - if (path == null) { - LOG.info("Binding not used/mounted! Skipping: " + id); - continue; - } + private void closeAllConnectors() { + if (this.isSdlBackendEnabled()) { + final GuacamoleConnector connector = (GuacamoleConnector) this.getControlConnector(GuacamoleConnector.PROTOCOL); + final GuacTunnel tunnel = (connector != null) ? connector.getTunnel() : null; + if (tunnel != null && tunnel.isOpen()) { + try { + tunnel.disconnect(); + tunnel.close(); + } catch (GuacamoleException error) { + LOG.log(Level.SEVERE, "Closing Guacamole connector failed!", error); + } + } + } else if (this.isXpraBackendEnabled()) { + final XpraConnector connector = (XpraConnector) this.getControlConnector(XpraConnector.PROTOCOL); + if (connector != null) { + try { + connector.disconnect(); + } catch (Exception error) { + LOG.log(Level.SEVERE, "Closing Xpra connector failed!", error); + } + } + } - images.put(id, path); - } + if (this.isPulseAudioEnabled()) { + final AudioConnector connector = (AudioConnector) this.getControlConnector(AudioConnector.PROTOCOL); + final IAudioStreamer streamer = (connector != null) ? connector.getAudioStreamer() : null; + if (streamer != null && !streamer.isClosed()) { + try { + streamer.stop(); + streamer.close(); + } catch (Exception error) { + LOG.log(Level.WARNING, "Stopping audio streamer failed!", error); + } + } + } + } + + void stopProcessRunner(ProcessRunner runner) { + final int emuProcessId = runner.getProcessId(); + final var emuContainerId = this.getContainerId(); + if (this.isContainerModeEnabled()) + LOG.info("Stopping emulator " + emuProcessId + " in container " + emuContainerId + "..."); + else LOG.info("Stopping emulator " + emuProcessId + "..."); + + try { + if (this.isSdlBackendEnabled()) { + // Send termination message + ctlMsgWriter.begin(MessageType.TERMINATE); + ctlMsgWriter.send(emuCtlSocketName); + + // Give emulator a chance to shutdown cleanly + if (runner.waitUntilFinished(5, TimeUnit.SECONDS)) { + LOG.info("Emulator " + emuProcessId + " stopped."); + return; + } + } + } catch (Exception exception) { + LOG.log(Level.SEVERE, "Stopping emulator failed!", exception); + } + + if (this.isContainerModeEnabled()) { + final var killer = new ProcessRunner(); + final var cmds = new ArrayList>(2); + cmds.add(List.of("runc", "kill", emuContainerId, "TERM")); + cmds.add(List.of("runc", "kill", "-a", emuContainerId, "KILL")); + for (final var args : cmds) { + killer.setCommand("sudo"); + killer.addArguments(args); + killer.setLogger(LOG); + killer.execute(); + + if (runner.waitUntilFinished(5, TimeUnit.SECONDS)) { + LOG.info("Emulator " + emuProcessId + " stopped."); + return; + } + } + } + + LOG.warning("Emulator " + emuProcessId + " failed to shutdown cleanly! Killing it..."); + runner.stop(5, TimeUnit.SECONDS); // Try to terminate the process + runner.kill(); // Try to kill the process + } + + @Override + public String getRuntimeConfiguration() throws BWFLAException { + synchronized (emuBeanState) { + if (emuBeanState.get() == EmuCompState.EMULATOR_UNDEFINED) { + String message = "Runtime configuration is not available in this state!"; + throw new IllegalEmulatorStateException(message, EmuCompState.EMULATOR_UNDEFINED) + .setId(this.getComponentId()); + } + } + try { + return this.emuEnvironment.value(); + } catch (JAXBException e) { + throw new BWFLAException("Serializing environment description failed!", e) + .setId(this.getComponentId()); + } + } + + @Override + public Set getColdplugableDrives() { + // TODO: here read result from corresponding metadata + return new HashSet(); + } + + @Override + public Set getHotplugableDrives() { + // TODO: here read result from corresponding metadata + return new HashSet(); + } + + private void sync() throws BWFLAException { + final ProcessRunner process = new ProcessRunner(); + process.setCommand("sync"); + if (!process.execute()) { + throw new BWFLAException("Syncing filesystem failed!") + .setId(this.getComponentId()); + } + + LOG.info("filesystem synced"); + } + + @Override + public List snapshot() throws BWFLAException { + synchronized (emuBeanState) { + final EmuCompState curstate = emuBeanState.get(); + if (curstate != EmuCompState.EMULATOR_STOPPED) { + String message = "Cannot save environment in this state!"; + throw new IllegalEmulatorStateException(message, curstate) + .setId(this.getComponentId()); + } + } + + // Collect all modified images for used/mounted bindings + final Map images = new LinkedHashMap(); + for (AbstractDataResource resource : emuEnvironment.getAbstractDataResource()) { + if (!(resource instanceof Binding)) + continue; + + final Binding binding = (Binding) resource; + if (((Binding) resource).getAccess() == Binding.AccessType.COPY) + continue; - // Now it should be safe to unmount! - this.unmountBindings(); - this.sync(); + final String id = binding.getId(); - // TODO: filter out all unchanged images with qemu-img compare! - // replace with client + final String path = bindings.lookup(BindingsManager.toBindingId(id, BindingsManager.EntryType.IMAGE)); + if (path == null) { + LOG.info("Binding not used/mounted! Skipping: " + id); + continue; + } + + images.put(id, path); + } + + // Now it should be safe to unmount! + this.unmountBindings(); + this.sync(); + + // TODO: filter out all unchanged images with qemu-img compare! + // replace with client // final BlobStore blobstore = BlobStoreClient.get() // .getBlobStorePort(blobStoreAddressSoap); - // Create one DataHandler per image - final List handlers = new ArrayList(); + // Create one DataHandler per image + final List handlers = new ArrayList(); // try { - for (Map.Entry entry : images.entrySet()) { - final String id = entry.getKey(); - final String path = entry.getValue(); - - final BlobDescription blob = new BlobDescription() - .setDescription("Snapshot for session " + this.getComponentId()) - .setNamespace("emulator-snapshots") - .setDataFromFile(Paths.get(path)) - .setType(".qcow") - .setName(id); - - // Upload image to blobstore and register cleanup handler - //TODO REPLACE WITH CLIENT - final BlobHandle handle = new BlobHandle(); + for (Map.Entry entry : images.entrySet()) { + final String id = entry.getKey(); + final String path = entry.getValue(); + + final BlobDescription blob = new BlobDescription() + .setDescription("Snapshot for session " + this.getComponentId()) + .setNamespace("emulator-snapshots") + .setDataFromFile(Paths.get(path)) + .setType(".qcow") + .setName(id); + + // Upload image to blobstore and register cleanup handler + //TODO REPLACE WITH CLIENT + final BlobHandle handle = new BlobHandle(); // cleanups.push("delete-blob/" + handle.getId(), () -> { // try { // blobstore.delete(handle); @@ -1401,13 +1347,13 @@ public List snapshot() throws BWFLAException // } // }); - final String location = handle.toRestUrl(blobStoreRestAddress); - final BindingDataHandler handler = new BindingDataHandler() - .setUrl(location) - .setId(id); + final String location = handle.toRestUrl(blobStoreRestAddress); + final BindingDataHandler handler = new BindingDataHandler() + .setUrl(location) + .setId(id); - handlers.add(handler); - } + handlers.add(handler); + } // } // catch (BWFLAException error) { // LOG.log(Level.WARNING, "Uploading images failed!", error); @@ -1415,41 +1361,39 @@ public List snapshot() throws BWFLAException // throw error; // } - return handlers; - } + return handlers; + } - @Override - public int changeMedium(int containerId, String objReference) throws BWFLAException - { - try { - LOG.info("change medium: " + objReference); - Drive drive = this.emuEnvironment.getDrive().get(containerId); - // detach the current medium - this.connectDrive(drive, false); + @Override + public int changeMedium(int containerId, String objReference) throws BWFLAException { + try { + LOG.info("change medium: " + objReference); + Drive drive = this.emuEnvironment.getDrive().get(containerId); + // detach the current medium + this.connectDrive(drive, false); - if (objReference == null || objReference.isEmpty()) { - return containerId; - } + if (objReference == null || objReference.isEmpty()) { + return containerId; + } - drive.setData(objReference); - boolean attachOk = (emuBeanState.fetch() == EmuCompState.EMULATOR_RUNNING) ? connectDrive(drive, true) : addDrive(drive); + drive.setData(objReference); + boolean attachOk = (emuBeanState.fetch() == EmuCompState.EMULATOR_RUNNING) ? connectDrive(drive, true) : addDrive(drive); - if (!attachOk) { - throw new BWFLAException("error occured in the last phase of device attachment") - .setId(this.getComponentId()); - } - } catch (IndexOutOfBoundsException e) { - throw new BWFLAException("Cannot change medium: invalid drive id given.", e) - .setId(this.getComponentId()); - } - // TODO: change disk in run-time - return containerId; - } - - @Override - @Deprecated - public int attachMedium(DataHandler data, String mediumType) throws BWFLAException - { + if (!attachOk) { + throw new BWFLAException("error occured in the last phase of device attachment") + .setId(this.getComponentId()); + } + } catch (IndexOutOfBoundsException e) { + throw new BWFLAException("Cannot change medium: invalid drive id given.", e) + .setId(this.getComponentId()); + } + // TODO: change disk in run-time + return containerId; + } + + @Override + @Deprecated + public int attachMedium(DataHandler data, String mediumType) throws BWFLAException { /* synchronized (emuBeanState) { @@ -1519,12 +1463,11 @@ public int attachMedium(DataHandler data, String mediumType) throws BWFLAExcepti return id; } */ - return -1; - } + return -1; + } - @Override - public DataHandler detachMedium(int containerId) throws BWFLAException - { + @Override + public DataHandler detachMedium(int containerId) throws BWFLAException { /* synchronized (emuBeanState) { @@ -1572,1115 +1515,1043 @@ public DataHandler detachMedium(int containerId) throws BWFLAException .setId(this.getComponentId()); */ - return null; - } - - - // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Protected - // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private EmulatorBeanMode getEmuBeanMode(MachineConfiguration config) throws IllegalArgumentException - { - final UiOptions options = config.getUiOptions(); - if (options != null && options.getForwarding_system() != null) { - var ret = EmulatorBeanMode.valueOf(options.getForwarding_system()); - if(ret == EmulatorBeanMode.HEADLESS && !this.isHeadlessSupported()) - return EmulatorBeanMode.SDLONP; - return ret; - } - else return EmulatorBeanMode.SDLONP; - } - - protected void prepareUserData(MachineConfiguration environment) throws BWFLAException { - File src = new File(emulatorDataBase + getEmuContainerName(environment)); - if(!src.exists()) { - LOG.info("no user data folder found " + src.getAbsolutePath()); - return; - } - - File userData = new File(getDataDir().toFile(), getEmuContainerName(environment)); - if(userData.exists()) - throw new BWFLAException("copied user data twice"); - - ProcessRunner cp = new ProcessRunner(); - cp.setCommand("cp"); - cp.addArgument("-rv"); - cp.addArgument(src.getAbsolutePath()); - cp.addArgument(userData.getAbsolutePath()); - if(!cp.execute()) - throw new BWFLAException("preparing emulator user data failed."); - } - - protected void setRuntimeConfiguration(MachineConfiguration environment) throws BWFLAException - { - try { - this.emuEnvironment = environment; - LOG.info(emuEnvironment.value()); - for (AbstractDataResource resource: emuEnvironment.getAbstractDataResource()) - this.prepareResource(resource); - - prepareUserData(environment); - - MachineConfiguration.NativeConfig nativeConfig = emuEnvironment.getNativeConfig(); - this.prepareNativeConfig(nativeConfig); - - UiOptions uiOptions = emuEnvironment.getUiOptions(); - if (uiOptions != null) { - this.isPulseAudioEnabled = false; - if(uiOptions.getAudio_system() != null - && !uiOptions.getAudio_system().isEmpty() - && uiOptions.getAudio_system().equalsIgnoreCase("webrtc")) { - this.isPulseAudioEnabled = true; - } - } - - this.prepareEmulatorRunner(); - - if (uiOptions != null) { - TimeOptions timeOptions = uiOptions.getTime(); - if (timeOptions != null) { - if (timeOptions.getEpoch() != null) { - long epoch = Long.parseLong(timeOptions.getEpoch()); - this.setEmulatorTime(epoch); - } else if (timeOptions.getOffset() != null) { - long offset = Long.parseLong(timeOptions.getEpoch()); - this.setEmulatorTime(offset); - } - } - } - - this.setupEmulatorBackend(); - - for(Drive drive: emuEnvironment.getDrive()) - prepareDrive(drive); - - for(Nic nic: emuEnvironment.getNic()) - prepareNic(nic); - - this.finishRuntimeConfiguration(); - - } catch (IllegalArgumentException | IOException | JAXBException e) { - throw new BWFLAException("Could not set runtime information.", e) - .setId(this.getComponentId()); - } - } - - protected String getNativeConfig() - { - return emuNativeConfig; - } - - /** Must be overriden by subclasses to initialize the emulator's command. */ - protected abstract void prepareEmulatorRunner() throws BWFLAException; - - /** Callback for performing actions, deferred during runtime configuration. */ - protected void finishRuntimeConfiguration() throws BWFLAException - { - // Do nothing! - } - - /** - * Determine the image format for a specified drive type for the current - * emulator. - *

- * This method should be overridden by any emulator that has specific - * file format needs (e.g. VirtualBox (yuck)). - * - * @param driveType The drive type - * @return The desired image format for the specified drive type - */ - protected XmountOutputFormat getImageFormatForDriveType(Drive.DriveType driveType) { - // as default, we use raw images for everything - return XmountOutputFormat.RAW; - } - - /** Setups the emulator's backend */ - private void setupEmulatorBackend() - { - switch (emuBeanMode) - { - case SDLONP: - this.setupEmulatorForSDLONP(); - break; - - case Y11: - this.setupEmulatorForY11(); - break; - - case XPRA: - // Nothing to setup! - } - } - - /** Setups the emulator's environment variables for running locally. */ - private void setupEmulatorForY11() - { - protocol = PROTOCOL_Y11; - - final String emusocket = this.getSocketsDir() - .resolve("sdlonp-iosocket-emu").toString(); - - // Setup emulator's tunnel - final GuacamoleConfiguration gconf = tunnelConfig.getGuacamoleConfiguration(); - gconf.setProtocol(PROTOCOL_SDLONP); - gconf.setParameter("enable-audio", "false"); - gconf.setParameter("emu-iosocket", emusocket); - - // Setup emulator's environment - emuRunner.addEnvVariable("SDL_VIDEODRIVER", protocol); - if (this.isPulseAudioEnabled()) - emuRunner.addEnvVariable("SDL_AUDIODRIVER", AUDIODRIVER_PULSE); - - emuRunner.addEnvVariable("SDL_SRVCTLSOCKET", ctlSocket.getName()); - emuRunner.addEnvVariable("SDL_EMUCTLSOCKET", emuCtlSocketName); - emuRunner.addEnvVariable("ALSA_CARD", alsa_card); - emuConfig.setIoSocket(emusocket); - - // TODO: should this parameter be read from meta-data? - emuConfig.setInactivityTimeout(this.getInactivityTimeout()); - - UiOptions uiopts = emuEnvironment.getUiOptions(); - if (uiopts == null) - return; - - Html5Options html5 = uiopts.getHtml5(); - if (html5 == null) - return; - - if (html5.isPointerLock()) - emuConfig.setRelativeMouse(true); - } - - private String fmtDate(long epoch) - { - Date d = new Date(epoch); - DateFormat format = new SimpleDateFormat("YYYY-MM-dd hh:mm:ss"); - String formatted = format.format(d); - return formatted; - } - - protected void setEmulatorTime(long epoch) - { - //LOG.info("set emulator time: " + epoch + " fmtStr " + fmtDate(epoch)); - // emuRunner.addEnvVariable("FAKETIME", ""+ fmtDate(epoch)); - // emuRunner.addEnvVariable("LD_PRELOAD", libfaketime); - } - - protected void setEmulatorTimeOffset(long offset) - { - // emuRunner.addEnvVariable("FAKE_TIME_OFFSET", ""+offset); - // emuRunner.addEnvVariable("LD_PRELOAD", "/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1"); - } - - /** Setups the emulator's environment variables and tunnel for SDLONP-Protocol. */ - private void setupEmulatorForSDLONP() - { - protocol = PROTOCOL_SDLONP; - - final String emusocket = this.getSocketsDir() - .resolve("sdlonp-iosocket-emu").toString(); - - // Setup emulator's tunnel - final GuacamoleConfiguration gconf = tunnelConfig.getGuacamoleConfiguration(); - gconf.setProtocol(protocol); - gconf.setParameter("enable-audio", Boolean.toString(!this.isPulseAudioEnabled())); - gconf.setParameter("emu-iosocket", emusocket); - - // Setup client configuration - if (!this.isPulseAudioEnabled()) { - final GuacamoleClientInformation ginfo = tunnelConfig.getGuacamoleClientInformation(); - ginfo.getAudioMimetypes().add("audio/ogg"); - } - - // Setup emulator's environment - emuRunner.addEnvVariable("SDL_AUDIODRIVER", (this.isPulseAudioEnabled()) ? AUDIODRIVER_PULSE : protocol); - emuRunner.addEnvVariable("SDL_VIDEODRIVER", protocol); - emuRunner.addEnvVariable("SDL_SRVCTLSOCKET", ctlSocket.getName()); - emuRunner.addEnvVariable("SDL_EMUCTLSOCKET", emuCtlSocketName); - emuConfig.setIoSocket(emusocket); - - // HACK: Qemu uses a custom audio setup! - if (this instanceof QemuBean && this.isPulseAudioEnabled() ) { - emuRunner.getEnvVariables() - .remove("SDL_AUDIODRIVER"); - } - - emuConfig.setInactivityTimeout(this.getInactivityTimeout()); - - UiOptions uiopts = emuEnvironment.getUiOptions(); - if (uiopts != null) { - Html5Options html5 = uiopts.getHtml5(); - if (html5 != null) { - if (html5.isPointerLock()) - emuConfig.setRelativeMouse(true); - - String crtopt = html5.getCrt(); - if (crtopt != null && !crtopt.isEmpty()) { - emuConfig.setCrtFilter("snes-ntsc"); - emuConfig.setCrtPreset("composite"); - } - } - - InputOptions input = uiopts.getInput(); - if (input != null) { - String kbdModel = input.getEmulatorKbdModel(); - if (kbdModel != null && !kbdModel.isEmpty()) - emuConfig.setKeyboardModel(kbdModel); - - String kbdLayout = input.getEmulatorKbdLayout(); - if (kbdLayout != null && !kbdLayout.isEmpty()) - emuConfig.setKeyboardLayout(kbdLayout); + return null; + } + + + // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Protected + // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + private EmulatorBeanMode getEmuBeanMode(MachineConfiguration config) throws IllegalArgumentException { + final UiOptions options = config.getUiOptions(); + if (options != null && options.getForwarding_system() != null) { + var ret = EmulatorBeanMode.valueOf(options.getForwarding_system()); + if (ret == EmulatorBeanMode.HEADLESS && !this.isHeadlessSupported()) + return EmulatorBeanMode.SDLONP; + return ret; + } else return EmulatorBeanMode.SDLONP; + } + + protected void prepareUserData(MachineConfiguration environment) throws BWFLAException { + File src = new File(emulatorDataBase + getEmuContainerName(environment)); + if (!src.exists()) { + LOG.info("no user data folder found " + src.getAbsolutePath()); + return; + } - String clientKbdModel = input.getClientKbdModel(); - if (clientKbdModel != null && !clientKbdModel.isEmpty()) - emuConfig.setClientKeyboardModel(clientKbdModel); + File userData = new File(getDataDir().toFile(), getEmuContainerName(environment)); + if (userData.exists()) + throw new BWFLAException("copied user data twice"); + + ProcessRunner cp = new ProcessRunner(); + cp.setCommand("cp"); + cp.addArgument("-rv"); + cp.addArgument(src.getAbsolutePath()); + cp.addArgument(userData.getAbsolutePath()); + if (!cp.execute()) + throw new BWFLAException("preparing emulator user data failed."); + } + + protected void setRuntimeConfiguration(MachineConfiguration environment) throws BWFLAException { + try { + this.emuEnvironment = environment; + LOG.info(emuEnvironment.value()); + for (AbstractDataResource resource : emuEnvironment.getAbstractDataResource()) + this.prepareResource(resource); + + prepareUserData(environment); + + MachineConfiguration.NativeConfig nativeConfig = emuEnvironment.getNativeConfig(); + this.prepareNativeConfig(nativeConfig); + + UiOptions uiOptions = emuEnvironment.getUiOptions(); + if (uiOptions != null) { + this.isPulseAudioEnabled = false; + if (uiOptions.getAudio_system() != null + && !uiOptions.getAudio_system().isEmpty() + && uiOptions.getAudio_system().equalsIgnoreCase("webrtc")) { + this.isPulseAudioEnabled = true; + } + } - String clientKbdLayout = input.getClientKbdLayout(); - if (clientKbdLayout != null && !clientKbdLayout.isEmpty()) - emuConfig.setClientKeyboardLayout(clientKbdLayout); - } - } - } + this.prepareEmulatorRunner(); + + if (uiOptions != null) { + TimeOptions timeOptions = uiOptions.getTime(); + if (timeOptions != null) { + if (timeOptions.getEpoch() != null) { + long epoch = Long.parseLong(timeOptions.getEpoch()); + this.setEmulatorTime(epoch); + } else if (timeOptions.getOffset() != null) { + long offset = Long.parseLong(timeOptions.getEpoch()); + this.setEmulatorTime(offset); + } + } + } + this.setupEmulatorBackend(); - /* ==================== EmuCon API ==================== */ + for (Drive drive : emuEnvironment.getDrive()) + prepareDrive(drive); - @Override - public DataHandler checkpoint() throws BWFLAException - { - if (!this.isContainerModeEnabled()) { - throw new BWFLAException("Container mode disabled! Checkpointing not possible.") - .setId(this.getComponentId()); - } + for (Nic nic : emuEnvironment.getNic()) + prepareNic(nic); - this.closeAllConnectors(); - if (this.isSdlBackendEnabled()) { - LOG.info("Waiting for emulator's detach-notification..."); - try { - this.waitForClientDetachAck(10, TimeUnit.SECONDS); - } catch (Exception e) { - throw new BWFLAException("Waiting for emulator's detach-notification failed!", e) - .setId(this.getComponentId()); - } - } + this.finishRuntimeConfiguration(); - final Path imgdir = this.getStateDir(); - try { - Files.createDirectories(imgdir); - } - catch (Exception error) { - throw new BWFLAException("Creating checkpoint directory failed!", error) - .setId(this.getComponentId()); - } - - final Path workdir = this.getWorkingDir(); - final Function relativizer = (abspath) -> { - final Path relpath = workdir.relativize(abspath); - return relpath.toString(); - }; + } catch (IllegalArgumentException | IOException | JAXBException e) { + throw new BWFLAException("Could not set runtime information.", e) + .setId(this.getComponentId()); + } + } + + protected String getNativeConfig() { + return emuNativeConfig; + } + + /** + * Must be overriden by subclasses to initialize the emulator's command. + */ + protected abstract void prepareEmulatorRunner() throws BWFLAException; + + /** + * Callback for performing actions, deferred during runtime configuration. + */ + protected void finishRuntimeConfiguration() throws BWFLAException { + // Do nothing! + } + + /** + * Determine the image format for a specified drive type for the current + * emulator. + *

+ * This method should be overridden by any emulator that has specific + * file format needs (e.g. VirtualBox (yuck)). + * + * @param driveType The drive type + * @return The desired image format for the specified drive type + */ + protected XmountOutputFormat getImageFormatForDriveType(Drive.DriveType driveType) { + // as default, we use raw images for everything + return XmountOutputFormat.RAW; + } + + /** + * Setups the emulator's backend + */ + private void setupEmulatorBackend() { + switch (emuBeanMode) { + case SDLONP: + this.setupEmulatorForSDLONP(); + break; + + case Y11: + this.setupEmulatorForY11(); + break; + + case XPRA: + // Nothing to setup! + } + } + + /** + * Setups the emulator's environment variables for running locally. + */ + private void setupEmulatorForY11() { + protocol = PROTOCOL_Y11; + + final String emusocket = this.getSocketsDir() + .resolve("sdlonp-iosocket-emu").toString(); + + // Setup emulator's tunnel + final GuacamoleConfiguration gconf = tunnelConfig.getGuacamoleConfiguration(); + gconf.setProtocol(PROTOCOL_SDLONP); + gconf.setParameter("enable-audio", "false"); + gconf.setParameter("emu-iosocket", emusocket); + + // Setup emulator's environment + emuRunner.addEnvVariable("SDL_VIDEODRIVER", protocol); + if (this.isPulseAudioEnabled()) + emuRunner.addEnvVariable("SDL_AUDIODRIVER", AUDIODRIVER_PULSE); + + emuRunner.addEnvVariable("SDL_SRVCTLSOCKET", ctlSocket.getName()); + emuRunner.addEnvVariable("SDL_EMUCTLSOCKET", emuCtlSocketName); + emuRunner.addEnvVariable("ALSA_CARD", alsa_card); + emuConfig.setIoSocket(emusocket); + + // TODO: should this parameter be read from meta-data? + emuConfig.setInactivityTimeout(this.getInactivityTimeout()); + + UiOptions uiopts = emuEnvironment.getUiOptions(); + if (uiopts == null) + return; + + Html5Options html5 = uiopts.getHtml5(); + if (html5 == null) + return; + + if (html5.isPointerLock()) + emuConfig.setRelativeMouse(true); + } + + private String fmtDate(long epoch) { + Date d = new Date(epoch); + DateFormat format = new SimpleDateFormat("YYYY-MM-dd hh:mm:ss"); + String formatted = format.format(d); + return formatted; + } + + protected void setEmulatorTime(long epoch) { + //LOG.info("set emulator time: " + epoch + " fmtStr " + fmtDate(epoch)); + // emuRunner.addEnvVariable("FAKETIME", ""+ fmtDate(epoch)); + // emuRunner.addEnvVariable("LD_PRELOAD", libfaketime); + } + + protected void setEmulatorTimeOffset(long offset) { + // emuRunner.addEnvVariable("FAKE_TIME_OFFSET", ""+offset); + // emuRunner.addEnvVariable("LD_PRELOAD", "/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1"); + } + + /** + * Setups the emulator's environment variables and tunnel for SDLONP-Protocol. + */ + private void setupEmulatorForSDLONP() { + protocol = PROTOCOL_SDLONP; + + final String emusocket = this.getSocketsDir() + .resolve("sdlonp-iosocket-emu").toString(); + + // Setup emulator's tunnel + final GuacamoleConfiguration gconf = tunnelConfig.getGuacamoleConfiguration(); + gconf.setProtocol(protocol); + gconf.setParameter("enable-audio", Boolean.toString(!this.isPulseAudioEnabled())); + gconf.setParameter("emu-iosocket", emusocket); + + // Setup client configuration + if (!this.isPulseAudioEnabled()) { + final GuacamoleClientInformation ginfo = tunnelConfig.getGuacamoleClientInformation(); + ginfo.getAudioMimetypes().add("audio/ogg"); + } - final Path checkpoint = workdir.resolve("checkpoint" + CHECKPOINT_FILE_EXTENSION); - final ProcessRunner process = new ProcessRunner(); + // Setup emulator's environment + emuRunner.addEnvVariable("SDL_AUDIODRIVER", (this.isPulseAudioEnabled()) ? AUDIODRIVER_PULSE : protocol); + emuRunner.addEnvVariable("SDL_VIDEODRIVER", protocol); + emuRunner.addEnvVariable("SDL_SRVCTLSOCKET", ctlSocket.getName()); + emuRunner.addEnvVariable("SDL_EMUCTLSOCKET", emuCtlSocketName); + emuConfig.setIoSocket(emusocket); + + // HACK: Qemu uses a custom audio setup! + if (this instanceof QemuBean && this.isPulseAudioEnabled()) { + emuRunner.getEnvVariables() + .remove("SDL_AUDIODRIVER"); + } - LOG.info("Checkpointing emulator-container " + this.getContainerId() + "..."); + emuConfig.setInactivityTimeout(this.getInactivityTimeout()); - // Try to checkpoint the container... - process.setCommand("emucon-checkpoint"); - process.addArgument("--non-interactive"); + UiOptions uiopts = emuEnvironment.getUiOptions(); + if (uiopts != null) { + Html5Options html5 = uiopts.getHtml5(); + if (html5 != null) { + if (html5.isPointerLock()) + emuConfig.setRelativeMouse(true); - for (String file : emuContainerFilesToCheckpoint) { - final Path path = Paths.get(file); - process.addArguments("--include", relativizer.apply(path)); - } + String crtopt = html5.getCrt(); + if (crtopt != null && !crtopt.isEmpty()) { + emuConfig.setCrtFilter("snes-ntsc"); + emuConfig.setCrtPreset("composite"); + } + } - process.addArguments("--image-dir", relativizer.apply(imgdir)); - process.addArguments("--output", checkpoint.toString()); - process.addArgument(this.getContainerId()); - process.setWorkingDirectory(workdir); - process.setLogger(LOG); - if (!process.execute()) { - throw new BWFLAException("Checkpointing emulator-container failed!") - .setId(this.getComponentId()); - } + InputOptions input = uiopts.getInput(); + if (input != null) { + String kbdModel = input.getEmulatorKbdModel(); + if (kbdModel != null && !kbdModel.isEmpty()) + emuConfig.setKeyboardModel(kbdModel); - return new DataHandler(new FileDataSource(checkpoint.toFile())); - } + String kbdLayout = input.getEmulatorKbdLayout(); + if (kbdLayout != null && !kbdLayout.isEmpty()) + emuConfig.setKeyboardLayout(kbdLayout); + String clientKbdModel = input.getClientKbdModel(); + if (clientKbdModel != null && !clientKbdModel.isEmpty()) + emuConfig.setClientKeyboardModel(clientKbdModel); - /* ==================== Session Recording Helpers ==================== */ + String clientKbdLayout = input.getClientKbdLayout(); + if (clientKbdLayout != null && !clientKbdLayout.isEmpty()) + emuConfig.setClientKeyboardLayout(clientKbdLayout); + } + } + } - public boolean prepareSessionRecorder() throws BWFLAException - { - if (recorder != null) { - LOG.info("SessionRecorder already prepared."); - return true; - } - if (player != null) { - String message = "Initialization of SessionRecorder failed, " - + "because SessionReplayer is already running. " - + "Using both at the same time is not supported!"; + /* ==================== EmuCon API ==================== */ - throw new BWFLAException(message) - .setId(this.getComponentId()); - } + @Override + public DataHandler checkpoint() throws BWFLAException { + if (!this.isContainerModeEnabled()) { + throw new BWFLAException("Container mode disabled! Checkpointing not possible.") + .setId(this.getComponentId()); + } - // Create and initialize the recorder - recorder = new SessionRecorder(this.getComponentId(), MESSAGE_BUFFER_CAPACITY); - try { - // Create and setup a temp-file for the recording - Path tmpfile = this.getDataDir().resolve(TRACE_FILE); - recorder.prepare(tmpfile); - } - catch (IOException exception) { - LOG.severe("Creation of output file for session-recording failed!"); - LOG.log(Level.SEVERE, exception.getMessage(), exception); - recorder = null; - return false; - } + this.closeAllConnectors(); + if (this.isSdlBackendEnabled()) { + LOG.info("Waiting for emulator's detach-notification..."); + try { + this.waitForClientDetachAck(10, TimeUnit.SECONDS); + } catch (Exception e) { + throw new BWFLAException("Waiting for emulator's detach-notification failed!", e) + .setId(this.getComponentId()); + } + } - // Register the recorder as interceptor - interceptors.addInterceptor(recorder); - - return true; - } - - public void startSessionRecording() throws BWFLAException - { - this.ensureRecorderIsInitialized(); - recorder.start(); - } - - public void stopSessionRecording() throws BWFLAException - { - this.ensureRecorderIsInitialized(); - recorder.stop(); - } - - public boolean isRecordModeEnabled() throws BWFLAException - { - if (recorder == null) - return false; - - return recorder.isRecording(); - } - - public void addActionFinishedMark() - { - // this.ensureRecorderIsInitialized(); - - InstructionBuilder ibuilder = new InstructionBuilder(16); - ibuilder.start(ExtOpCode.ACTION_FINISHED); - ibuilder.finish(); - - recorder.postMessage(SourceType.INTERNAL, ibuilder.array(), 0, ibuilder.length()); - } - - /** Add a new metadata chunk to the trace-file. */ - public void defineTraceMetadataChunk(String tag, String comment) throws BWFLAException - { - this.ensureRecorderIsInitialized(); - recorder.defineMetadataChunk(tag, comment); - } - - /** Add a key/value pair as metadata to the trace-file. */ - public void addTraceMetadataEntry(String ctag, String key, String value) throws BWFLAException - { - this.ensureRecorderIsInitialized(); - recorder.addMetadataEntry(ctag, key, value); - } - - public String getSessionTrace() throws BWFLAException - { - this.ensureRecorderIsInitialized(); - - try { - recorder.finish(); - } - catch (IOException exception) { - LOG.severe("Finishing session-recording failed!"); - LOG.log(Level.SEVERE, exception.getMessage(), exception); - return null; - } + final Path imgdir = this.getStateDir(); + try { + Files.createDirectories(imgdir); + } catch (Exception error) { + throw new BWFLAException("Creating checkpoint directory failed!", error) + .setId(this.getComponentId()); + } - return recorder.toString(); - } + final Path workdir = this.getWorkingDir(); + final Function relativizer = (abspath) -> { + final Path relpath = workdir.relativize(abspath); + return relpath.toString(); + }; - private void ensureRecorderIsInitialized() throws BWFLAException - { - if (recorder == null) { - throw new BWFLAException("SessionRecorder is not initialized!") - .setId(this.getComponentId()); - } - } + final Path checkpoint = workdir.resolve("checkpoint" + CHECKPOINT_FILE_EXTENSION); + final ProcessRunner process = new ProcessRunner(); + LOG.info("Checkpointing emulator-container " + this.getContainerId() + "..."); - /* ==================== Session Replay Helpers ==================== */ + // Try to checkpoint the container... + process.setCommand("emucon-checkpoint"); + process.addArgument("--non-interactive"); - public boolean prepareSessionPlayer(String trace, boolean headless) throws BWFLAException - { - if (player != null) { - LOG.info("SessionPlayer already prepared."); - return true; - } + for (String file : emuContainerFilesToCheckpoint) { + final Path path = Paths.get(file); + process.addArguments("--include", relativizer.apply(path)); + } - if (recorder != null) { - String message = "Initialization of SessionPlayer failed, " - + "because SessionRecorder is already running. " - + "Using both at the same time is not supported!"; + process.addArguments("--image-dir", relativizer.apply(imgdir)); + process.addArguments("--output", checkpoint.toString()); + process.addArgument(this.getContainerId()); + process.setWorkingDirectory(workdir); + process.setLogger(LOG); + if (!process.execute()) { + throw new BWFLAException("Checkpointing emulator-container failed!") + .setId(this.getComponentId()); + } - throw new BWFLAException(message) - .setId(this.getComponentId()); - } + return new DataHandler(new FileDataSource(checkpoint.toFile())); + } - Path file = this.getDataDir().resolve(TRACE_FILE); - try { - FileUtils.writeStringToFile(file.toFile(), trace); - } - catch (IOException exception) { - LOG.severe("An error occured while writing temporary session-trace!"); - LOG.log(Level.SEVERE, exception.getMessage(), exception); - return false; - } - player = new SessionPlayerWrapper(file, headless); + /* ==================== Session Recording Helpers ==================== */ - return true; - } + public boolean prepareSessionRecorder() throws BWFLAException { + if (recorder != null) { + LOG.info("SessionRecorder already prepared."); + return true; + } - public int getSessionPlayerProgress() - { - if (player == null) - return 0; + if (player != null) { + String message = "Initialization of SessionRecorder failed, " + + "because SessionReplayer is already running. " + + "Using both at the same time is not supported!"; - return player.getProgress(); - } + throw new BWFLAException(message) + .setId(this.getComponentId()); + } - public boolean isReplayModeEnabled() - { - if (player == null) - return false; + // Create and initialize the recorder + recorder = new SessionRecorder(this.getComponentId(), MESSAGE_BUFFER_CAPACITY); + try { + // Create and setup a temp-file for the recording + Path tmpfile = this.getDataDir().resolve(TRACE_FILE); + recorder.prepare(tmpfile); + } catch (IOException exception) { + LOG.severe("Creation of output file for session-recording failed!"); + LOG.log(Level.SEVERE, exception.getMessage(), exception); + recorder = null; + return false; + } - return player.isPlaying(); - } + // Register the recorder as interceptor + interceptors.addInterceptor(recorder); + + return true; + } + + public void startSessionRecording() throws BWFLAException { + this.ensureRecorderIsInitialized(); + recorder.start(); + } + + public void stopSessionRecording() throws BWFLAException { + this.ensureRecorderIsInitialized(); + recorder.stop(); + } + + public boolean isRecordModeEnabled() throws BWFLAException { + if (recorder == null) + return false; + + return recorder.isRecording(); + } + + public void addActionFinishedMark() { + // this.ensureRecorderIsInitialized(); + + InstructionBuilder ibuilder = new InstructionBuilder(16); + ibuilder.start(ExtOpCode.ACTION_FINISHED); + ibuilder.finish(); + + recorder.postMessage(SourceType.INTERNAL, ibuilder.array(), 0, ibuilder.length()); + } + + /** + * Add a new metadata chunk to the trace-file. + */ + public void defineTraceMetadataChunk(String tag, String comment) throws BWFLAException { + this.ensureRecorderIsInitialized(); + recorder.defineMetadataChunk(tag, comment); + } + + /** + * Add a key/value pair as metadata to the trace-file. + */ + public void addTraceMetadataEntry(String ctag, String key, String value) throws BWFLAException { + this.ensureRecorderIsInitialized(); + recorder.addMetadataEntry(ctag, key, value); + } + + public String getSessionTrace() throws BWFLAException { + this.ensureRecorderIsInitialized(); + + try { + recorder.finish(); + } catch (IOException exception) { + LOG.severe("Finishing session-recording failed!"); + LOG.log(Level.SEVERE, exception.getMessage(), exception); + return null; + } - /* ==================== Monitoring API ==================== */ + return recorder.toString(); + } - @Override - public boolean updateMonitorValues() - { - ProcessMonitor monitor = emuRunner.getProcessMonitor(); - if (monitor == null) { - // Process is currently not running! - return false; - } + private void ensureRecorderIsInitialized() throws BWFLAException { + if (recorder == null) { + throw new BWFLAException("SessionRecorder is not initialized!") + .setId(this.getComponentId()); + } + } - return monitor.update(); - } - @Override - public String getMonitorValue(ProcessMonitorVID id) - { - ProcessMonitor monitor = emuRunner.getProcessMonitor(); - if (monitor == null) { - // Process is currently not running! - return ProcessMonitor.INVALID_VALUE; - } + /* ==================== Session Replay Helpers ==================== */ - return monitor.getValue(id); - } + public boolean prepareSessionPlayer(String trace, boolean headless) throws BWFLAException { + if (player != null) { + LOG.info("SessionPlayer already prepared."); + return true; + } - @Override - public List getMonitorValues(Collection ids) - { - ProcessMonitor monitor = emuRunner.getProcessMonitor(); - if (monitor == null) { - // Process is currently not running! - return ProcessMonitor.INVALID_VALUE_LIST; - } + if (recorder != null) { + String message = "Initialization of SessionPlayer failed, " + + "because SessionRecorder is already running. " + + "Using both at the same time is not supported!"; - return monitor.getValues(ids); - } + throw new BWFLAException(message) + .setId(this.getComponentId()); + } - @Override - public List getAllMonitorValues() - { - ProcessMonitor monitor = emuRunner.getProcessMonitor(); - if (monitor == null) { - // Process is currently not running! - return ProcessMonitor.INVALID_VALUE_LIST; - } + Path file = this.getDataDir().resolve(TRACE_FILE); + try { + FileUtils.writeStringToFile(file.toFile(), trace); + } catch (IOException exception) { + LOG.severe("An error occured while writing temporary session-trace!"); + LOG.log(Level.SEVERE, exception.getMessage(), exception); + return false; + } - return monitor.getValues(); - } - - - /* ==================== PostScriptPrinter API ==================== */ - - public List getPrintJobs() throws BWFLAException{ - if(printer == null) - return null; - - return printer.getPrintJobs(); - } - - /* ==================== Screenshot API ==================== */ - - public void takeScreenshot() - { - if(scrshooter != null) - scrshooter.takeScreenshot(); - } - - public DataHandler getNextScreenshot() - { - if(scrshooter == null) - return null; - - byte[] data = scrshooter.getNextScreenshot(); - if (data == null) - return null; - - return new DataHandler(data, "application/octet-stream"); - } - - - // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Utilities - // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - // - // private int allocateContainerId(Container container) - // { - // int freeId = -1; - // final int MAX_TRIES = 50; - // for(int i = 0; (i < MAX_TRIES) && (freeId == -1); ++i) - // { - // freeId = (new Random()).nextInt(); - // if(containers.containsKey(freeId)) - // freeId = -1; - // else - // containers.put(freeId, container); - // } - // - // return freeId; - // } - - private boolean sendEmulatorConfig() - { - try { - emuConfig.sendAllTo(ctlSocket, emuCtlSocketName); - } - catch (Exception exception) { - LOG.warning("Sending configuration to emulator failed!"); - LOG.log(Level.SEVERE, exception.getMessage(), exception); - emuBeanState.update(EmuCompState.EMULATOR_FAILED); - return false; - } + player = new SessionPlayerWrapper(file, headless); - return true; - } + return true; + } - private void ensureEmuCompState(EmuCompState expstate, String msgsuffix) throws BWFLAException - { - if (emuBeanState.get() != expstate) { - throw new BWFLAException("Expected state changed, abort waiting for " + msgsuffix + "!") - .setId(this.getComponentId()); - } - } + public int getSessionPlayerProgress() { + if (player == null) + return 0; - private void ensureEmulatorRunning(String msgsuffix) throws BWFLAException - { - if (!emuRunner.isProcessRunning()) { - throw new BWFLAException("Emulator failed, abort waiting for " + msgsuffix + "!") - .setId(this.getComponentId()); - } - } + return player.getProgress(); + } - private boolean waitForReadyNotification(int expevent, String message, int timeout, EmuCompState expstate) - { - LOG.info(message); + public boolean isReplayModeEnabled() { + if (player == null) + return false; - try { - final int waittime = 1000; // in ms - int numretries = (timeout > waittime) ? timeout / waittime : 1; - boolean isMsgAvailable = false; - while (numretries > 0) { - isMsgAvailable = ctlMsgReader.read(waittime); - if (isMsgAvailable) - break; + return player.isPlaying(); + } - this.ensureEmulatorRunning("notification"); + /* ==================== Monitoring API ==================== */ - if (emuBeanState.get() != expstate) { - LOG.warning("Expected state changed, abort waiting for notification!"); - return false; - } + @Override + public boolean updateMonitorValues() { + ProcessMonitor monitor = emuRunner.getProcessMonitor(); + if (monitor == null) { + // Process is currently not running! + return false; + } - --numretries; - } + return monitor.update(); + } - if (!isMsgAvailable) { - LOG.warning("Reading from emulator timed out!"); - return false; - } + @Override + public String getMonitorValue(ProcessMonitorVID id) { + ProcessMonitor monitor = emuRunner.getProcessMonitor(); + if (monitor == null) { + // Process is currently not running! + return ProcessMonitor.INVALID_VALUE; + } - if (!ctlMsgReader.isNotification()) { - LOG.warning("Received message was not a notification!"); - return false; - } + return monitor.getValue(id); + } - if (ctlMsgReader.getEventID() != expevent) { - LOG.warning("Received an unexpected notification from emulator!"); - return false; - } - } - catch (Exception exception) { - LOG.warning("Failed to read a notification-message from emulator."); - LOG.log(Level.SEVERE, exception.getMessage(), exception); - return false; - } + @Override + public List getMonitorValues(Collection ids) { + ProcessMonitor monitor = emuRunner.getProcessMonitor(); + if (monitor == null) { + // Process is currently not running! + return ProcessMonitor.INVALID_VALUE_LIST; + } - LOG.info("Received a ready-notification from emulator."); + return monitor.getValues(ids); + } - return true; - } + @Override + public List getAllMonitorValues() { + ProcessMonitor monitor = emuRunner.getProcessMonitor(); + if (monitor == null) { + // Process is currently not running! + return ProcessMonitor.INVALID_VALUE_LIST; + } - private void waitUntilEmulatorCtlSocketAvailable(EmuCompState expstate) throws BWFLAException - { - LOG.info("Waiting for emulator's control-socket to become available..."); + return monitor.getValues(); + } + + + /* ==================== PostScriptPrinter API ==================== */ + + public List getPrintJobs() throws BWFLAException { + if (printer == null) + return null; + + return printer.getPrintJobs(); + } + + /* ==================== Screenshot API ==================== */ + + public void takeScreenshot() { + if (scrshooter != null) + scrshooter.takeScreenshot(); + } + + public DataHandler getNextScreenshot() { + if (scrshooter == null) + return null; + + byte[] data = scrshooter.getNextScreenshot(); + if (data == null) + return null; + + return new DataHandler(data, "application/octet-stream"); + } + + + // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Utilities + // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // private int allocateContainerId(Container container) + // { + // int freeId = -1; + // final int MAX_TRIES = 50; + // for(int i = 0; (i < MAX_TRIES) && (freeId == -1); ++i) + // { + // freeId = (new Random()).nextInt(); + // if(containers.containsKey(freeId)) + // freeId = -1; + // else + // containers.put(freeId, container); + // } + // + // return freeId; + // } + + private boolean sendEmulatorConfig() { + try { + emuConfig.sendAllTo(ctlSocket, emuCtlSocketName); + } catch (Exception exception) { + LOG.warning("Sending configuration to emulator failed!"); + LOG.log(Level.SEVERE, exception.getMessage(), exception); + emuBeanState.update(EmuCompState.EMULATOR_FAILED); + return false; + } - final Path socket = Paths.get(emuCtlSocketName); - final int timeout = 30000; // in ms - final int waittime = 1000; // in ms - for (int numretries = timeout / waittime; numretries > 0; --numretries) { - if (Files.exists(socket)) { - LOG.info("Emulator's control-socket is now available."); - return; - } + return true; + } - try { - Thread.sleep(waittime); - } - catch (Exception error) { - // Ignore it! - } + private void ensureEmuCompState(EmuCompState expstate, String msgsuffix) throws BWFLAException { + if (emuBeanState.get() != expstate) { + throw new BWFLAException("Expected state changed, abort waiting for " + msgsuffix + "!") + .setId(this.getComponentId()); + } + } - final String msgsuffix = "emulator's control-socket"; - this.ensureEmuCompState(expstate, msgsuffix); - this.ensureEmulatorRunning(msgsuffix); - } + private void ensureEmulatorRunning(String msgsuffix) throws BWFLAException { + if (!emuRunner.isProcessRunning()) { + throw new BWFLAException("Emulator failed, abort waiting for " + msgsuffix + "!") + .setId(this.getComponentId()); + } + } - emuBeanState.update(EmuCompState.EMULATOR_FAILED); - throw new BWFLAException("Emulator's control socket is not available!") - .setId(this.getComponentId()); - } + private boolean waitForReadyNotification(int expevent, String message, int timeout, EmuCompState expstate) { + LOG.info(message); - private void waitUntilEmulatorCtlSocketReady(EmuCompState expstate) throws BWFLAException - { - final int timeout = 60000; // in ms + try { + final int waittime = 1000; // in ms + int numretries = (timeout > waittime) ? timeout / waittime : 1; + boolean isMsgAvailable = false; + while (numretries > 0) { + isMsgAvailable = ctlMsgReader.read(waittime); + if (isMsgAvailable) + break; - final String message = "Waiting for emulator's control-socket to become ready..."; - boolean ok = this.waitForReadyNotification(EventID.EMULATOR_CTLSOCK_READY, message, timeout, expstate); - if (!ok) { - emuBeanState.update(EmuCompState.EMULATOR_FAILED); - throw new BWFLAException("Emulator's control socket is not reachable!") - .setId(this.getComponentId()); - } - } + this.ensureEmulatorRunning("notification"); - private boolean waitUntilEmulatorReady(EmuCompState expstate) throws BWFLAException - { - final int timeout = 30000; // in ms + if (emuBeanState.get() != expstate) { + LOG.warning("Expected state changed, abort waiting for notification!"); + return false; + } - final String message = "Waiting for emulator to become ready..."; - boolean ok = this.waitForReadyNotification(EventID.EMULATOR_READY, message, timeout, expstate); - if (!ok) { - emuBeanState.update(EmuCompState.EMULATOR_FAILED); - throw new BWFLAException("Emulator was not started properly!") - .setId(this.getComponentId()); - } + --numretries; + } - return ok; - } + if (!isMsgAvailable) { + LOG.warning("Reading from emulator timed out!"); + return false; + } - private void waitUntilPathExists(Path path, EmuCompState expstate) throws BWFLAException - { - LOG.info("Waiting for path '" + path.toString() +"'..."); + if (!ctlMsgReader.isNotification()) { + LOG.warning("Received message was not a notification!"); + return false; + } - final int timeout = 180000; // in ms - final int waittime = 1000; // in ms - for (int numretries = timeout / waittime; numretries > 0; --numretries) { - if (Files.exists(path)) { - LOG.info("Path '" + path.toString() +"' exists now"); - return; - } + if (ctlMsgReader.getEventID() != expevent) { + LOG.warning("Received an unexpected notification from emulator!"); + return false; + } + } catch (Exception exception) { + LOG.warning("Failed to read a notification-message from emulator."); + LOG.log(Level.SEVERE, exception.getMessage(), exception); + return false; + } - try { - Thread.sleep(waittime); - } - catch (Exception error) { - // Ignore it! - } + LOG.info("Received a ready-notification from emulator."); - final String msgsuffix = "path"; - this.ensureEmuCompState(expstate, msgsuffix); - this.ensureEmulatorRunning(msgsuffix); - } + return true; + } - throw new BWFLAException("Path '" + path.toString() +"' does not exist!") - .setId(this.getComponentId()); - } - - private void waitUntilRestoreDone() - { - LOG.info("Waiting for CRIU restore-worker to exit..."); - - final ProcessRunner waiter = new ProcessRunner("/bin/sh"); - waiter.addArguments("-c", "while ! sudo runc " + - "ps " + this.getContainerId() + "; do :; done; while sudo runc ps " + this.getContainerId() + " | grep -q criu; do :; done"); - waiter.execute(); - } - - private void attachClientToEmulator() throws IOException, InterruptedException - { - LOG.info("Attaching client to emulator..."); - Thread.sleep(1000); - synchronized (ctlMsgWriter) { - ctlMsgWriter.begin(MessageType.ATTACH_CLIENT); - ctlMsgWriter.send(emuCtlSocketName); - } - } - - private void waitForAttachedClient() throws IOException, InterruptedException - { - final int timeout = 1000; - int numretries = 30; - - // Wait for the attached-event from emulator - while (numretries > 0) { - if (ctlEvents.await(EventID.CLIENT_ATTACHED, timeout)) { - LOG.info("Client attached to emulator."); - isClientAttachedFlag.set(true); - return; // Notification received! - } - - final EmuCompState state = emuBeanState.fetch(); - if (state != EmuCompState.EMULATOR_BUSY && state != EmuCompState.EMULATOR_RUNNING) { - LOG.warning("Expected state changed, abort attaching client to emulator!"); - return; - } + private void waitUntilEmulatorCtlSocketAvailable(EmuCompState expstate) throws BWFLAException { + LOG.info("Waiting for emulator's control-socket to become available..."); - --numretries; - } + final Path socket = Paths.get(emuCtlSocketName); + final int timeout = 30000; // in ms + final int waittime = 1000; // in ms + for (int numretries = timeout / waittime; numretries > 0; --numretries) { + if (Files.exists(socket)) { + LOG.info("Emulator's control-socket is now available."); + return; + } - throw new IOException("Attaching client to emulator failed!"); - } + try { + Thread.sleep(waittime); + } catch (Exception error) { + // Ignore it! + } - private void waitForClientDetachAck(long timeout, TimeUnit unit) throws IOException, InterruptedException - { - // Wait for the detached-event from emulator - if (!ctlEvents.await(EventID.CLIENT_DETACHED, unit.toMillis(timeout))) - throw new IOException("No detach-notification received from emulator!"); + final String msgsuffix = "emulator's control-socket"; + this.ensureEmuCompState(expstate, msgsuffix); + this.ensureEmulatorRunning(msgsuffix); + } - isClientAttachedFlag.set(false); - } + emuBeanState.update(EmuCompState.EMULATOR_FAILED); + throw new BWFLAException("Emulator's control socket is not available!") + .setId(this.getComponentId()); + } - private String newCtlSocketName(String suffix) - { - return this.getSocketsDir() - .resolve("sdlonp-ctlsocket-" + suffix) - .toString(); - } + private void waitUntilEmulatorCtlSocketReady(EmuCompState expstate) throws BWFLAException { + final int timeout = 60000; // in ms - protected BWFLAException newNotSupportedException() - { - return new BWFLAException("Operation is not supported!") - .setId(this.getComponentId()); - } + final String message = "Waiting for emulator's control-socket to become ready..."; + boolean ok = this.waitForReadyNotification(EventID.EMULATOR_CTLSOCK_READY, message, timeout, expstate); + if (!ok) { + emuBeanState.update(EmuCompState.EMULATOR_FAILED); + throw new BWFLAException("Emulator's control socket is not reachable!") + .setId(this.getComponentId()); + } + } - protected BWFLAException newNotImplementedException() - { - return new BWFLAException("Operation is not implemented!") - .setId(this.getComponentId()); - } + private boolean waitUntilEmulatorReady(EmuCompState expstate) throws BWFLAException { + final int timeout = 30000; // in ms - private BWFLAException newInitFailureException(String message, Throwable error) - { - emuBeanState.update(EmuCompState.EMULATOR_FAILED); - LOG.log(Level.SEVERE, message, error); - return new BWFLAException(message, error) - .setId(this.getComponentId()); - } + final String message = "Waiting for emulator to become ready..."; + boolean ok = this.waitForReadyNotification(EventID.EMULATOR_READY, message, timeout, expstate); + if (!ok) { + emuBeanState.update(EmuCompState.EMULATOR_FAILED); + throw new BWFLAException("Emulator was not started properly!") + .setId(this.getComponentId()); + } - private ImageArchiveBinding findEmulatorImage(MachineConfiguration env) throws Exception - { - final var imageArchiveAddress = ConfigProvider.getConfig() - .getValue("rest.imagearchive", String.class); + return ok; + } + private void waitUntilPathExists(Path path, EmuCompState expstate) throws BWFLAException { + LOG.info("Waiting for path '" + path.toString() + "'..."); - //TODO REPLACE WITH CLIENT - return new ImageArchiveBinding(); + final int timeout = 180000; // in ms + final int waittime = 1000; // in ms + for (int numretries = timeout / waittime; numretries > 0; --numretries) { + if (Files.exists(path)) { + LOG.info("Path '" + path.toString() + "' exists now"); + return; + } + try { + Thread.sleep(waittime); + } catch (Exception error) { + // Ignore it! + } -// try (final var archive = ImageArchiveClient.create(imageArchiveAddress)) { -// final var emuMetaHelper = new EmulatorMetaHelperV2(archive, LOG); -// final var name = this.getEmuContainerName(env); -// var version = env.getEmulator() -// .getContainerVersion(); -// -// if (version == null || version.isEmpty()) -// version = EmulatorMetaData.DEFAULT_VERSION; -// -// LOG.info("Looking up image for emulator '" + name + " (" + version + ")'..."); -// final var emulator = emuMetaHelper.fetch(name, version); -// final var image = emulator.image(); -// final var binding = new ImageArchiveBinding(); -// binding.setId(EMUCON_ROOTFS_BINDING_ID); -// binding.setAccess(Binding.AccessType.COW); -// binding.setBackendName(this.getEmulatorArchive()); -// binding.setFileSystemType(image.fileSystemType()); -// binding.setType(image.category()); -// binding.setImageId(image.id()); -// binding.setUrl(""); -// -// LOG.info("Using emulator's image '" + image.id() + "'"); -// return binding; -// } -// catch (Exception error) { -// throw new BWFLAException("Emulator's image not found!", error) -// .setId(this.getComponentId()); -// } - } - - - /************************************************************************** - * - * Here be Bindings - * - **************************************************************************/ - - /** - * Resolves a binding location of either the form - * binding://binding_id[/path/to/subres] or binding_id[/path/to/subres]. The - * binding_id is replaced with the actual filesystem location of the - * binding's mountpoint. The possible reference to the subresource is - * preserved in the returned string. - * - * @param binding - * A binding location - * @return The resolved path or null, if the binding cannot - * be found - */ - protected String lookupResource(String binding) throws BWFLAException, IOException - { - String mountpoint = bindings.lookup(binding); - if (mountpoint == null) - mountpoint = bindings.mount(this.getComponentId(), binding, this.getBindingsDir()); - - return mountpoint; - } - - @Deprecated - protected String lookupResource(String binding, XmountOutputFormat unused) throws BWFLAException, IOException { - return lookupResource(binding); - } - - @Deprecated - protected String lookupResource(String binding, Drive.DriveType driveType) - throws BWFLAException, IOException { - // this.getImageFormatForDriveType(driveType) - return this.lookupResource(binding); - } - - protected void prepareResource(AbstractDataResource resource) throws IllegalArgumentException, IOException, BWFLAException - { - bindings.register(resource); - - // NOTE: Premount all object's entries to allow media-changes inside containers... - if (this.isContainerModeEnabled() && (resource instanceof ObjectArchiveBinding)) { - bindings.find(resource.getId() + "/") - .forEach((binding) -> { - try { - this.lookupResource(binding); - } - catch (Exception error) { - throw new IllegalArgumentException(error); - } - }); - } - } - - /************************************************************************** - * - * Here be Drives - * - **************************************************************************/ - - /** - * @param drive - */ - protected void prepareDrive(Drive drive) throws BWFLAException - { - // All drives *directly* work on a resource (binding) that has been - // set up earlier, so no mounting, cow-ing or other tricks - // are necessary here. - - if(drive.getData() == null || drive.getData().isEmpty()) - return; - - addDrive(drive); - - // String img = null; - // - // FIXME: check if this is still necessary after refactoring (if yes, - // refactor more) - // - // if (drive instanceof VolatileDrive) { - // // The drive should be written to in-place, ignoring the - // // value of getAccess(), as it is a temporary copy of user-data - // - // // (TODO) Currently only file: transport is allowed here - // if (!drive.getData().startsWith("file:")) { - // log. - // warning("Only 'file:' transport is allowed for injected objects/VolatileDrives."); - // continue; - // } - // // just use the file on the filesystem directly as is - // img = drive.getData().replace("file://", ""); - // } else { - - } - - protected abstract boolean addDrive(Drive drive) throws BWFLAException; - - protected abstract boolean connectDrive(Drive drive, boolean attach) throws BWFLAException; - - /************************************************************************** - * - * Here be Networks - * - **************************************************************************/ - - /** - * @param nic - */ - protected void prepareNic(Nic nic) throws BWFLAException, IOException - { - // create a vde_switch in hub mode - // the switch can later be identified using the NIC's MAC address - Path vdeHubName = this.getNetworksDir() - .resolve("nic_" + nic.getHwaddress()); - - if (this.isContainerModeEnabled()) { - // Pre-create switch-directory to be mounted into container! - Files.createDirectories(vdeHubName); - - // Compute switch-directory in container-space - final Path hostDataDir = this.getDataDir(); - final Path conDataDir = Paths.get(EMUCON_DATA_DIR); - vdeHubName = conDataDir.resolve(hostDataDir.relativize(vdeHubName)); - } - else { - ProcessRunner process = new ProcessRunner("vde_switch"); - process.addArgument("-hub"); - process.addArgument("-s"); - process.addArgument(vdeHubName.toString()); - if (!process.start()) - return; // Failure - - vdeProcesses.add(process); - } + final String msgsuffix = "path"; + this.ensureEmuCompState(expstate, msgsuffix); + this.ensureEmulatorRunning(msgsuffix); + } - this.addControlConnector(new EthernetConnector(nic.getHwaddress(), vdeHubName, LOG, this)); - this.addNic(nic); - } + throw new BWFLAException("Path '" + path.toString() + "' does not exist!") + .setId(this.getComponentId()); + } + + private void waitUntilRestoreDone() { + LOG.info("Waiting for CRIU restore-worker to exit..."); + + final ProcessRunner waiter = new ProcessRunner("/bin/sh"); + waiter.addArguments("-c", "while ! sudo runc " + + "ps " + this.getContainerId() + "; do :; done; while sudo runc ps " + this.getContainerId() + " | grep -q criu; do :; done"); + waiter.execute(); + } + + private void attachClientToEmulator() throws IOException, InterruptedException { + LOG.info("Attaching client to emulator..."); + Thread.sleep(1000); + synchronized (ctlMsgWriter) { + ctlMsgWriter.begin(MessageType.ATTACH_CLIENT); + ctlMsgWriter.send(emuCtlSocketName); + } + } + + private void waitForAttachedClient() throws IOException, InterruptedException { + final int timeout = 1000; + int numretries = 30; + + // Wait for the attached-event from emulator + while (numretries > 0) { + if (ctlEvents.await(EventID.CLIENT_ATTACHED, timeout)) { + LOG.info("Client attached to emulator."); + isClientAttachedFlag.set(true); + return; // Notification received! + } - protected abstract boolean addNic(Nic nic) throws BWFLAException; + final EmuCompState state = emuBeanState.fetch(); + if (state != EmuCompState.EMULATOR_BUSY && state != EmuCompState.EMULATOR_RUNNING) { + LOG.warning("Expected state changed, abort attaching client to emulator!"); + return; + } - /************************************************************************** - * - * Here be native config - * - **************************************************************************/ + --numretries; + } - /** - * @param nativeConfig - */ - protected void prepareNativeConfig(MachineConfiguration.NativeConfig nativeConfig) - { - if(nativeConfig != null) - { - String nativeString = nativeConfig.getValue(); - if(nativeConfig.getLinebreak() != null) - { - nativeString = nativeString.replace(nativeConfig.getLinebreak(), "\n"); - } + throw new IOException("Attaching client to emulator failed!"); + } + + private void waitForClientDetachAck(long timeout, TimeUnit unit) throws IOException, InterruptedException { + // Wait for the detached-event from emulator + if (!ctlEvents.await(EventID.CLIENT_DETACHED, unit.toMillis(timeout))) + throw new IOException("No detach-notification received from emulator!"); + + isClientAttachedFlag.set(false); + } + + private String newCtlSocketName(String suffix) { + return this.getSocketsDir() + .resolve("sdlonp-ctlsocket-" + suffix) + .toString(); + } + + protected BWFLAException newNotSupportedException() { + return new BWFLAException("Operation is not supported!") + .setId(this.getComponentId()); + } + + protected BWFLAException newNotImplementedException() { + return new BWFLAException("Operation is not implemented!") + .setId(this.getComponentId()); + } + + private BWFLAException newInitFailureException(String message, Throwable error) { + emuBeanState.update(EmuCompState.EMULATOR_FAILED); + LOG.log(Level.SEVERE, message, error); + return new BWFLAException(message, error) + .setId(this.getComponentId()); + } + + /** + * Used to find Emulator Image if EMUCON_ROOTFS_BINDING_ID -> emucon-rootfs is absent + * Using latest configuration + * + * @return ImageArchiveBinding + * @throws Exception + */ + private ImageArchiveBinding findEmulatorImage() throws Exception { + + Optional imageArchiveBindingDeclaration = BindingsResolver.findFirstImageArchiveBindingDeclaration(); + if (imageArchiveBindingDeclaration.isEmpty()) { + throw new BWFLAException(String.format("Emulator's image not found!")); + } - nativeString = nativeString.replace("\r", "") - .replaceAll("\n+", "\n"); + ImageArchiveBinding binding = imageArchiveBindingDeclaration.get(); + binding.setId(EMUCON_ROOTFS_BINDING_ID); + binding.setAccess(Binding.AccessType.COW); + binding.setBackendName(this.getEmulatorArchive()); + binding.setUrl(""); + + LOG.info("Using emulator's image '" + binding.getId() + "'"); + + return binding; + } + + + /************************************************************************** + * + * Here be Bindings + * + **************************************************************************/ + + /** + * Resolves a binding location of either the form + * binding://binding_id[/path/to/subres] or binding_id[/path/to/subres]. The + * binding_id is replaced with the actual filesystem location of the + * binding's mountpoint. The possible reference to the subresource is + * preserved in the returned string. + * + * @param binding A binding location + * @return The resolved path or null, if the binding cannot + * be found + */ + protected String lookupResource(String binding) throws BWFLAException, IOException { + String mountpoint = bindings.lookup(binding); + if (mountpoint == null) + mountpoint = bindings.mount(this.getComponentId(), binding, this.getBindingsDir()); + + return mountpoint; + } + + @Deprecated + protected String lookupResource(String binding, XmountOutputFormat unused) throws BWFLAException, IOException { + return lookupResource(binding); + } + + @Deprecated + protected String lookupResource(String binding, Drive.DriveType driveType) + throws BWFLAException, IOException { + // this.getImageFormatForDriveType(driveType) + return this.lookupResource(binding); + } + + protected void prepareResource(AbstractDataResource resource) throws IllegalArgumentException, IOException, BWFLAException { + bindings.register(resource); + + // NOTE: Premount all object's entries to allow media-changes inside containers... + if (this.isContainerModeEnabled() && (resource instanceof ObjectArchiveBinding)) { + bindings.find(resource.getId() + "/") + .forEach((binding) -> { + try { + this.lookupResource(binding); + } catch (Exception error) { + throw new IllegalArgumentException(error); + } + }); + } + } + + /************************************************************************** + * + * Here be Drives + * + **************************************************************************/ + + /** + * @param drive + */ + protected void prepareDrive(Drive drive) throws BWFLAException { + // All drives *directly* work on a resource (binding) that has been + // set up earlier, so no mounting, cow-ing or other tricks + // are necessary here. + + if (drive.getData() == null || drive.getData().isEmpty()) + return; + + addDrive(drive); + + // String img = null; + // + // FIXME: check if this is still necessary after refactoring (if yes, + // refactor more) + // + // if (drive instanceof VolatileDrive) { + // // The drive should be written to in-place, ignoring the + // // value of getAccess(), as it is a temporary copy of user-data + // + // // (TODO) Currently only file: transport is allowed here + // if (!drive.getData().startsWith("file:")) { + // log. + // warning("Only 'file:' transport is allowed for injected objects/VolatileDrives."); + // continue; + // } + // // just use the file on the filesystem directly as is + // img = drive.getData().replace("file://", ""); + // } else { + + } + + protected abstract boolean addDrive(Drive drive) throws BWFLAException; + + protected abstract boolean connectDrive(Drive drive, boolean attach) throws BWFLAException; + + /************************************************************************** + * + * Here be Networks + * + **************************************************************************/ + + /** + * @param nic + */ + protected void prepareNic(Nic nic) throws BWFLAException, IOException { + // create a vde_switch in hub mode + // the switch can later be identified using the NIC's MAC address + Path vdeHubName = this.getNetworksDir() + .resolve("nic_" + nic.getHwaddress()); + + if (this.isContainerModeEnabled()) { + // Pre-create switch-directory to be mounted into container! + Files.createDirectories(vdeHubName); + + // Compute switch-directory in container-space + final Path hostDataDir = this.getDataDir(); + final Path conDataDir = Paths.get(EMUCON_DATA_DIR); + vdeHubName = conDataDir.resolve(hostDataDir.relativize(vdeHubName)); + } else { + ProcessRunner process = new ProcessRunner("vde_switch"); + process.addArgument("-hub"); + process.addArgument("-s"); + process.addArgument(vdeHubName.toString()); + if (!process.start()) + return; // Failure + + vdeProcesses.add(process); + } - // search for binding:// and replace all occurrences with the - // actual path - // Pattern p = Pattern.compile("binding://(\\w*/?)"); - Pattern p = Pattern.compile("binding://(\\w*/?)|rom://(\\S+)"); - Matcher m = p.matcher(nativeString); - StringBuffer sb = new StringBuffer(); - while(m.find()) - { - String bindingPath; - try { - String res = m.group(1); - if(res == null) // should be a rom. but check again - { - if(!m.group(0).startsWith("rom")) - { - LOG.info("could not resolve resource: " + m.group(0)); - continue; - } - res = m.group(0); - } + this.addControlConnector(new EthernetConnector(nic.getHwaddress(), vdeHubName, LOG, this)); + this.addNic(nic); + } + + protected abstract boolean addNic(Nic nic) throws BWFLAException; + + /************************************************************************** + * + * Here be native config + * + **************************************************************************/ + + /** + * @param nativeConfig + */ + protected void prepareNativeConfig(MachineConfiguration.NativeConfig nativeConfig) { + if (nativeConfig != null) { + String nativeString = nativeConfig.getValue(); + if (nativeConfig.getLinebreak() != null) { + nativeString = nativeString.replace(nativeConfig.getLinebreak(), "\n"); + } - bindingPath = this.lookupResource(res.trim()); - } catch (Exception e) { - LOG.severe("lookupResource with " + m.group(1) + " failed."); - LOG.log(Level.SEVERE, e.getMessage(), e); - continue; - } - if(bindingPath == null) - { - LOG.severe("lookupResource with " + m.group(1) + " failed."); - continue; - } - LOG.info(m.group(1)); - LOG.info("Replacing " + m.group(0) + " by " + bindingPath); - m.appendReplacement(sb, bindingPath); - } - m.appendTail(sb); + nativeString = nativeString.replace("\r", "") + .replaceAll("\n+", "\n"); + + // search for binding:// and replace all occurrences with the + // actual path + // Pattern p = Pattern.compile("binding://(\\w*/?)"); + Pattern p = Pattern.compile("binding://(\\w*/?)|rom://(\\S+)"); + Matcher m = p.matcher(nativeString); + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String bindingPath; + try { + String res = m.group(1); + if (res == null) // should be a rom. but check again + { + if (!m.group(0).startsWith("rom")) { + LOG.info("could not resolve resource: " + m.group(0)); + continue; + } + res = m.group(0); + } + + bindingPath = this.lookupResource(res.trim()); + } catch (Exception e) { + LOG.severe("lookupResource with " + m.group(1) + " failed."); + LOG.log(Level.SEVERE, e.getMessage(), e); + continue; + } + if (bindingPath == null) { + LOG.severe("lookupResource with " + m.group(1) + " failed."); + continue; + } + LOG.info(m.group(1)); + LOG.info("Replacing " + m.group(0) + " by " + bindingPath); + m.appendReplacement(sb, bindingPath); + } + m.appendTail(sb); - emuNativeConfig = sb.toString(); - } - } - - protected String getEmulatorWorkdir() - { - return null; - } - - public Tail getEmulatorStdOut() - { - LogConnector logCon = (LogConnector)getControlConnector(StdoutLogConnector.PROTOCOL); - if(logCon == null) - return null; - return logCon.connect(); - } - - public Tail getEmulatorStdErr() - { - LogConnector logCon = (LogConnector)getControlConnector(StderrLogConnector.PROTOCOL); - if(logCon == null) - return null; - return logCon.connect(); - } + emuNativeConfig = sb.toString(); + } + } + + protected String getEmulatorWorkdir() { + return null; + } + + public Tail getEmulatorStdOut() { + LogConnector logCon = (LogConnector) getControlConnector(StdoutLogConnector.PROTOCOL); + if (logCon == null) + return null; + return logCon.connect(); + } + + public Tail getEmulatorStdErr() { + LogConnector logCon = (LogConnector) getControlConnector(StderrLogConnector.PROTOCOL); + if (logCon == null) + return null; + return logCon.connect(); + } } diff --git a/emucomp-impl/src/main/resources/config/test-machine.json b/emucomp-impl/src/main/resources/config/test-machine.json index 7a3a5d4..442cf29 100755 --- a/emucomp-impl/src/main/resources/config/test-machine.json +++ b/emucomp-impl/src/main/resources/config/test-machine.json @@ -1,103 +1,131 @@ { "type": "emulationEnvironment", - "arch" : "x86_64", - "emulator" : { - "bean" : "Qemu", - "containerName" : "qemu-system" + "arch": "x86_64", + "emulator": { + "bean": "Qemu", + "containerName": "qemu-system" }, - "ui_options" : { - "html5" : { - "pointer_lock" : false + "ui_options": { + "html5": { + "pointer_lock": false }, - "input" : { - "clientKbdLayout" : "us", - "required" : false + "input": { + "clientKbdLayout": "us", + "required": false }, - "audio_system" : "webRTC", - "disableGhostCursor" : false + "audio_system": "webRTC", + "disableGhostCursor": false }, - "operatingSystemId" : "os:linux:ubuntu", - "drive" : [ { - "unit" : "0", - "type" : "floppy", - "filesystem" : "fat12", - "boot" : false, - "plugged" : false - }, { - "unit" : "1", - "type" : "floppy", - "filesystem" : "fat12", - "boot" : false, - "plugged" : false - }, { - "data" : "binding://", - "iface" : "ide", - "bus" : "0", - "unit" : "0", - "type" : "disk", - "boot" : true, - "plugged" : false - }, { - "data" : "binding:///", - "iface" : "ide", - "bus" : "0", - "unit" : "1", - "type" : "cdrom", - "filesystem" : "ISO", - "boot" : false, - "plugged" : false - }, { - "iface" : "ide", - "bus" : "1", - "unit" : "0", - "type" : "disk", - "boot" : false, - "plugged" : false - }, { - "iface" : "ide", - "bus" : "1", - "unit" : "1", - "type" : "cdrom", - "filesystem" : "ISO", - "boot" : false, - "plugged" : false - } ], - "nic" : [ { - "hwaddress" : "c6:7c:66:b0:21:d5" - } ], - "abstractDataResource" : [ { - "imageArchiveBinding" : { - "imageId" : "", - "type" : "user", - "backendName" : "default", - "filesize" : -1, - "id" : "emucon-rootfs" + "operatingSystemId": "os:linux:ubuntu", + "drive": [ + { + "unit": "0", + "type": "floppy", + "filesystem": "fat12", + "boot": false, + "plugged": false + }, + { + "unit": "1", + "type": "floppy", + "filesystem": "fat12", + "boot": false, + "plugged": false + }, + { + "data": "binding://", + "iface": "ide", + "bus": "0", + "unit": "0", + "type": "disk", + "boot": true, + "plugged": false + }, + { + "data": "binding:///", + "iface": "ide", + "bus": "0", + "unit": "1", + "type": "cdrom", + "filesystem": "ISO", + "boot": false, + "plugged": false + }, + { + "iface": "ide", + "bus": "1", + "unit": "0", + "type": "disk", + "boot": false, + "plugged": false + }, + { + "iface": "ide", + "bus": "1", + "unit": "1", + "type": "cdrom", + "filesystem": "ISO", + "boot": false, + "plugged": false + } + ], + "nic": [ + { + "hwaddress": "c6:7c:66:b0:21:d5" } - }, { - "imageArchiveBinding" : { - "imageId" : "", - "type" : "user", - "backendName" : "default", - "filesize" : -1, - "id" : "" + ], + "abstractDataResource": [ + { + "imageArchiveBinding": { + "imageId": "", + "type": "user", + "backendName": "default", + "filesize": -1, + "id": "emucon-rootfs" + } + }, + { + "imageArchiveBinding": { + "imageId": "", + "type": "user", + "backendName": "default", + "filesize": -1, + "id": "" + } + }, + { + "objectArchiveBinding": { + "objectId": "", + "archive": "default", + "id": "" + } } - },{ - "objectArchiveBinding" : { - "objectId" : "", - "archive" : "default", - "id" : "" + ], + "attachedFiles": [ + { + "id": "", + "label": "file-label", + "files": [ + { + "type": "cdrom", + "resourceType": "disk", + "isDefault": true, + "archive": "default", + "objectId": "" + } + ] } - } ], - "nativeConfig" : { - "value" : "-vga cirrus -smp 1 -net nic,model=rtl8139 -soundhw ac97 -m 1024 -usb -usbdevice tablet" + ], + "nativeConfig": { + "value": "-vga cirrus -smp 1 -net nic,model=rtl8139 -soundhw ac97 -m 1024 -usb -usbdevice tablet" }, - "isLinuxRuntime" : false, - "description" : { - "title" : "test environment" + "isLinuxRuntime": false, + "description": { + "title": "test environment" }, - "configurationType" : "MachineConfigurationTemplate", - "metaDataVersion" : "1", - "deleted" : false, - "id" : "test-machine", - "timestamp" : "2023-09-05T17:46:29.483217Z" + "configurationType": "MachineConfigurationTemplate", + "metaDataVersion": "1", + "deleted": false, + "id": "test-machine", + "timestamp": "2023-09-05T17:46:29.483217Z" } \ No newline at end of file