diff --git a/Dockerfile b/Dockerfile index 4effccb..4102427 100755 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,6 @@ RUN useradd -ms /bin/bash --uid 1000 --gid bwfla bwfla && for grp in fuse disk a RUN chown bwfla:bwfla /home/bwfla -USER bwfla - RUN mkdir -p /home/bwfla/.bwFLA \ /home/bwfla/demo-ui \ /home/bwfla/image-archive \ @@ -26,12 +24,17 @@ RUN mkdir -p /home/bwfla/.bwFLA \ /home/bwfla/export \ /home/bwfla/defaults -RUN cd /tmp/emucon-tools-master && \ - . ./bootstrap.sh && \ + +WORKDIR /tmp/emucon-tools-master +RUN . ./bootstrap.sh && \ + mkdir -p /bin/builder && \ + ln -sf /tmp/emucon-tools-master/builder/ /bin/builder/ && \ + mkdir -p /bin/installer && \ + ln -sf /tmp/emucon-tools-master/installer/install-oci-tools.sh /bin/installer/install-oci-tools.sh && \ . ./install.sh --destination /usr/local -u bwfla && \ - ./installer/install-oci-tools.sh --destination /tmp/oci-tools && \ ./installer/install-deps.sh + FROM maven:3.8.6-eclipse-temurin-11 AS build WORKDIR /app @@ -49,7 +52,7 @@ FROM eclipse-temurin:11-jdk-jammy RUN echo "locales locales/default_environment_locale string en_US.UTF-8" | debconf-set-selections RUN echo "keyboard-configuration keyboard-configuration/layoutcode string us" | debconf-set-selections RUN DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y xpra socat vde2 qemu-utils qemu-system ntfs-3g util-linux sudo unzip +RUN apt-get update && apt-get install -y xpra socat vde2 qemu-utils qemu-system ntfs-3g util-linux sudo unzip pulseaudio pulseaudio-utils RUN rm -rf /var/lib/apt/lists/* RUN mkdir -p /linapple-pie /minivmac /usr/local/bin 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 27c460c..eae06ac 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 @@ -10,7 +10,12 @@ @Getter @XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "fileCollection", namespace = "http://bwfla.bwl.de/common/datatypes") +@XmlType(name = "fileCollection", namespace = "http://bwfla.bwl.de/common/datatypes", propOrder = { + "id", + "files", + "archive", + "label" +}) @XmlRootElement(namespace = "http://bwfla.bwl.de/common/datatypes") public class FileCollection extends JaxbType { @XmlElement(name = "file", namespace = "http://bwfla.bwl.de/common/datatypes") 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 80e6cdf..3d47638 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 @@ -48,6 +48,7 @@ "drive", "nic", "abstractDataResource", + "attachedFiles", "nativeConfig", "outputBindingId", "isLinuxRuntime" @@ -78,7 +79,7 @@ public class MachineConfiguration @XmlElementRef(name = "binding", type = Binding.class, namespace = "http://bwfla.bwl.de/common/datatypes"), @XmlElementRef(name = "objectArchiveBinding", type = ObjectArchiveBinding.class, namespace = "http://bwfla.bwl.de/common/datatypes")}) protected List abstractDataResource; - + @XmlElement(namespace = "http://bwfla.bwl.de/common/datatypes") protected List attachedFiles; @XmlElement(namespace = "http://bwfla.bwl.de/common/datatypes") 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 index 33d228d..c795eda 100644 --- 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 @@ -1,10 +1,7 @@ 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 de.bwl.bwfla.emucomp.common.*; import lombok.extern.slf4j.Slf4j; import java.io.File; @@ -134,9 +131,9 @@ private static void loadConfiguration(String path) { String data = new String(is.readAllBytes(), StandardCharsets.UTF_8); extractedConfigurationHolder = new StringBuffer(data); - extractedComponentConfiguration = mapperThreadLocal.get().readValue(data, ComponentConfiguration.class); + extractedComponentConfiguration = mapperThreadLocal.get().readValue(data, MachineConfiguration.class); } catch (IOException e) { - log.error("Cannot load configuration from {}", path); + log.error("Cannot load configuration from {}", path, e); } } } 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 59ec958..1ea3591 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 @@ -45,6 +45,10 @@ import de.bwl.bwfla.emucomp.template.BlobHandle; import de.bwl.bwfla.emucomp.xpra.IAudioStreamer; import de.bwl.bwfla.emucomp.xpra.PulseAudioStreamer; +import de.bwl.bwfla.emucomp.xpra.XpraUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.glyptodon.guacamole.GuacamoleException; @@ -79,6 +83,7 @@ /** * @author iv1004 */ +@Slf4j public abstract class EmulatorBean extends EaasComponentBean implements EmulatorComponent { private EmulatorBeanMode emuBeanMode; @@ -97,7 +102,7 @@ public abstract class EmulatorBean extends EaasComponentBean implements Emulator // allow beans to disable the fake clock preload protected boolean disableFakeClock = false; - private boolean isPulseAudioEnabled = false; + private boolean isPulseAudioEnabled = ConfigProvider.getConfig().getValue("emucomp.enable_pulseaudio", Boolean.class); @Inject protected ThreadFactory workerThreadFactory; @@ -552,6 +557,12 @@ private void startBackend() throws BWFLAException, IOException { 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); @@ -1486,11 +1497,9 @@ protected void setRuntimeConfiguration(MachineConfiguration environment) throws 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; + if (uiOptions.getAudio_system() == null || uiOptions.getAudio_system().isEmpty()) { + this.isPulseAudioEnabled = false; + LOG.info("PulseAudio cannot be enabled, audio system is not initialized."); } } diff --git a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/control/FilterDispatcher.java b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/control/FilterDispatcher.java index 991dfce..c97fb8a 100755 --- a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/control/FilterDispatcher.java +++ b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/control/FilterDispatcher.java @@ -86,8 +86,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } case AudioConnector.PROTOCOL: { - // use the custom signalling servlet for incoming requests - servletName = WebRtcSignallingServlet.SERVLET_NAME; + // WebRTC signalling servlet commented out - using Xpra native audio instead + // servletName = WebRtcSignallingServlet.SERVLET_NAME; break; } } diff --git a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/control/WebRtcSignallingServlet.java b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/control/WebRtcSignallingServlet.java index 4f0d457..e795fbd 100755 --- a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/control/WebRtcSignallingServlet.java +++ b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/control/WebRtcSignallingServlet.java @@ -19,180 +19,119 @@ package de.bwl.bwfla.emucomp.control; -import de.bwl.bwfla.emucomp.NodeManager; -import de.bwl.bwfla.emucomp.common.exceptions.BWFLAException; -import de.bwl.bwfla.emucomp.components.AbstractEaasComponent; -import de.bwl.bwfla.emucomp.control.connectors.AudioConnector; -import de.bwl.bwfla.emucomp.control.connectors.IConnector; -import de.bwl.bwfla.emucomp.xpra.IAudioStreamer; - -import javax.inject.Inject; -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; import java.util.logging.Logger; - -// Note that this servlet does not have any URL pattern (neither in web.xml) -// The dispatching to this servlet is done in the FilterDispatcher -@WebServlet(name = WebRtcSignallingServlet.SERVLET_NAME) +/** + * WebRTC Signalling Servlet - DISABLED + */ +@Deprecated +//@WebServlet(name = WebRtcSignallingServlet.SERVLET_NAME) public class WebRtcSignallingServlet extends HttpServlet { public static final String SERVLET_NAME = "WebRtcSignallingServlet"; - /** - * Protocol ID, that must be present in request's URL - */ - private static final String PROTOCOL_SUFFIX = "/" + AudioConnector.PROTOCOL; - - /** - * Length of the Protocol ID - */ - private static final int PROTOCOL_SUFFIX_LENGTH = PROTOCOL_SUFFIX.length(); - - /** - * Start offset of a component ID in the request's URL - */ - private static final int COMPONENT_ID_OFFSET = "/components/".length(); - - /** - * Logger instance. - */ private final Logger log = Logger.getLogger(SERVLET_NAME); - @Inject - protected NodeManager nodeManager; - - - protected String getComponentId(HttpServletRequest request) throws WebRtcSignallingException { - // Parse the request's path, that should contain the session's ID - final String path = request.getPathInfo(); - if (path == null || !path.endsWith(PROTOCOL_SUFFIX)) - throw new WebRtcSignallingException(HttpServletResponse.SC_BAD_REQUEST, "Wrong servlet requested!"); - - final int soffset = COMPONENT_ID_OFFSET; - final int eoffset = path.length() - PROTOCOL_SUFFIX_LENGTH; - final String componentId = path.substring(soffset, eoffset); - if (componentId.isEmpty()) - throw new WebRtcSignallingException(HttpServletResponse.SC_BAD_REQUEST, "Component ID is missing in request!"); - - return componentId; - } - - protected AudioConnector getAudioConnector(String componentId) throws WebRtcSignallingException { - try { - AbstractEaasComponent component = nodeManager.getComponentById(componentId, AbstractEaasComponent.class); - IConnector connector = component.getControlConnector(AudioConnector.PROTOCOL); - if (!(connector instanceof AudioConnector)) { - String message = "No AudioConnector found for component '" + componentId + "'!"; - throw new WebRtcSignallingException(HttpServletResponse.SC_NOT_FOUND, message); - } - - return (AudioConnector) connector; - } catch (BWFLAException error) { - final String message = "No component found with ID " + componentId; - throw new WebRtcSignallingException(HttpServletResponse.SC_NOT_FOUND, message, error); - } - } - - - /* =============== HttpServlet Implementation =============== */ - @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { - try { - final String compid = this.getComponentId(request); - final AudioConnector connector = this.getAudioConnector(compid); - final IAudioStreamer streamer = connector.getAudioStreamer(); - if (streamer == null) { - final String message = "No AudioStreamer found for component " + compid + "!"; - throw new WebRtcSignallingException(HttpServletResponse.SC_NOT_FOUND, message); - } - - response.setContentType("application/json"); - response.addHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Cache-Control", "no-cache"); - - final String message = streamer.pollServerControlMessage(30, TimeUnit.SECONDS); - if (message == null) { - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - return; - } - response.getWriter().write(message); - } catch (Exception error) { - log.log(Level.WARNING, "Forwarding S2C control-message failed!", error); - final int httpcode = (error instanceof WebRtcSignallingException) ? - ((WebRtcSignallingException) error).getHttpCode() : HttpServletResponse.SC_INTERNAL_SERVER_ERROR; - - response.sendError(httpcode, error.getMessage()); - } +// try { +// final String compid = this.getComponentId(request); +// final AudioConnector connector = this.getAudioConnector(compid); +// final IAudioStreamer streamer = connector.getAudioStreamer(); +// if (streamer == null) { +// final String message = "No AudioStreamer found for component " + compid + "!"; +// throw new WebRtcSignallingException(HttpServletResponse.SC_NOT_FOUND, message); +// } +// +// response.setContentType("application/json"); +// response.addHeader("Access-Control-Allow-Origin", "*"); +// response.setHeader("Cache-Control", "no-cache"); +// +// final String message = streamer.pollServerControlMessage(30, TimeUnit.SECONDS); +// if (message == null) { +// response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); +// return; +// } +// response.getWriter().write(message); +// } catch (Exception error) { +// log.log(Level.WARNING, "Forwarding S2C control-message failed!", error); +// final int httpcode = (error instanceof WebRtcSignallingException) ? +// ((WebRtcSignallingException) error).getHttpCode() : HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +// +// response.sendError(httpcode, error.getMessage()); +// } + log.warning("WebRTC signalling servlet accessed but WebRTC is disabled - using Xpra native audio instead"); + response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, + "WebRTC functionality has been disabled. Audio is now handled natively by Xpra."); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { - try { - final String compid = this.getComponentId(request); - final AudioConnector connector = this.getAudioConnector(compid); - final String query = request.getQueryString(); - if (query != null && query.equals("connect")) { - log.info("New audio stream was requested for component " + compid); - connector.newAudioStreamer() - .play(); - - response.addHeader("Access-Control-Allow-Origin", "*"); - response.setStatus(HttpServletResponse.SC_OK); - return; - } - - final char[] buffer = new char[request.getContentLength()]; - final int length = request.getReader() - .read(buffer); - - if (length != buffer.length) - throw new IOException("Reading payload failed! Expected " + buffer.length + " bytes, received " + length); - - final IAudioStreamer streamer = connector.getAudioStreamer(); - if (streamer == null) { - final String message = "No AudioStreamer found for component " + compid + "!"; - throw new WebRtcSignallingException(HttpServletResponse.SC_NOT_FOUND, message); - } - - streamer.postClientControlMessage(buffer); - - response.addHeader("Access-Control-Allow-Origin", "*"); - response.setStatus(HttpServletResponse.SC_OK); - } catch (Exception error) { - log.log(Level.WARNING, "Forwarding C2S control-message failed!", error); - final int httpcode = (error instanceof WebRtcSignallingException) ? - ((WebRtcSignallingException) error).getHttpCode() : HttpServletResponse.SC_INTERNAL_SERVER_ERROR; - - response.sendError(httpcode, error.getMessage()); - } + log.warning("WebRTC signalling servlet accessed but WebRTC is disabled - using Xpra native audio instead"); + response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, + "WebRTC functionality has been disabled. Audio is now handled natively by Xpra."); +// try { +// final String compid = this.getComponentId(request); +// final AudioConnector connector = this.getAudioConnector(compid); +// final String query = request.getQueryString(); +// if (query != null && query.equals("connect")) { +// log.info("New audio stream was requested for component " + compid); +// connector.newAudioStreamer() +// .play(); +// +// response.addHeader("Access-Control-Allow-Origin", "*"); +// response.setStatus(HttpServletResponse.SC_OK); +// return; +// } +// +// final char[] buffer = new char[request.getContentLength()]; +// final int length = request.getReader() +// .read(buffer); +// +// if (length != buffer.length) +// throw new IOException("Reading payload failed! Expected " + buffer.length + " bytes, received " + length); +// +// final IAudioStreamer streamer = connector.getAudioStreamer(); +// if (streamer == null) { +// final String message = "No AudioStreamer found for component " + compid + "!"; +// throw new WebRtcSignallingException(HttpServletResponse.SC_NOT_FOUND, message); +// } +// +// streamer.postClientControlMessage(buffer); +// +// response.addHeader("Access-Control-Allow-Origin", "*"); +// response.setStatus(HttpServletResponse.SC_OK); +// } catch (Exception error) { +// log.log(Level.WARNING, "Forwarding C2S control-message failed!", error); +// final int httpcode = (error instanceof WebRtcSignallingException) ? +// ((WebRtcSignallingException) error).getHttpCode() : HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +// +// response.sendError(httpcode, error.getMessage()); +// } } - private static class WebRtcSignallingException extends ServletException { - private final int code; - - - public WebRtcSignallingException(int code, String message) { - super(message); - - this.code = code; - } - - public WebRtcSignallingException(int code, String message, Throwable cause) { - super(message, cause); - - this.code = code; - } - - public int getHttpCode() { - return code; - } - } +// private static class WebRtcSignallingException extends ServletException { +// private final int code; +// +// +// public WebRtcSignallingException(int code, String message) { +// super(message); +// +// this.code = code; +// } +// +// public WebRtcSignallingException(int code, String message, Throwable cause) { +// super(message, cause); +// +// this.code = code; +// } +// +// public int getHttpCode() { +// return code; +// } +// } } diff --git a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/Configuration.java b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/Configuration.java deleted file mode 100755 index d3acd48..0000000 --- a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/Configuration.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.bwl.bwfla.emucomp.xpra; - - -public class Configuration { - - public static final int[] PORT_RANGE = {7000,7015}; -} diff --git a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/PulseAudioStreamer.java b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/PulseAudioStreamer.java index 45a76d6..5d67cc6 100755 --- a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/PulseAudioStreamer.java +++ b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/PulseAudioStreamer.java @@ -20,50 +20,49 @@ package de.bwl.bwfla.emucomp.xpra; import de.bwl.bwfla.emucomp.common.logging.PrefixLogger; -import org.freedesktop.gstreamer.*; -import org.freedesktop.gstreamer.webrtc.WebRTCBin; -import org.freedesktop.gstreamer.webrtc.WebRTCBin.CREATE_OFFER; -import org.freedesktop.gstreamer.webrtc.WebRTCBin.ON_ICE_CANDIDATE; -import org.freedesktop.gstreamer.webrtc.WebRTCBin.ON_NEGOTIATION_NEEDED; -import org.freedesktop.gstreamer.webrtc.WebRTCSDPType; -import org.freedesktop.gstreamer.webrtc.WebRTCSessionDescription; - -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonReader; -import java.io.StringReader; -import java.lang.ref.WeakReference; + import java.nio.file.Path; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; import java.util.logging.Logger; /** - * Code is based on GStreamer's WebRTC demos: + * PulseAudio streamer for Xpra - WebRTC functionality has been disabled. + * Audio is now handled natively by Xpra using PulseAudio. + *

+ * Original WebRTC code was based on GStreamer's WebRTC demos: * https://github.com/centricular/gstwebrtc-demos */ -public class PulseAudioStreamer implements IAudioStreamer -{ - private final Logger log; +public class PulseAudioStreamer implements IAudioStreamer { + private final Logger log; + private final Path pulsesock; + private final BlockingQueue outqueue; - private final BlockingQueue outqueue; + // WebRTC components commented out - using Xpra native audio instead + /* private final Pipeline pipeline; private final WebRTCBin webrtc; private final Bin audio; - private boolean closed; + */ - private static final long OUTPUT_QUEUE_OFFER_TIMEOUT = 5L; + private boolean closed; + private boolean playing; + private static final long OUTPUT_QUEUE_OFFER_TIMEOUT = 5L; - public PulseAudioStreamer(String cid, Path pulsesock) - { - final PrefixLogger logger = new PrefixLogger(PulseAudioStreamer.class.getName()); - logger.getContext().add("cid", cid); - this.log = logger; + public PulseAudioStreamer(String cid, Path pulsesock) { + final PrefixLogger logger = new PrefixLogger(PulseAudioStreamer.class.getName()); + logger.getContext().add("cid", cid); + this.log = logger; + this.pulsesock = pulsesock; + + log.info("Initializing PulseAudioStreamer for Xpra native audio (WebRTC disabled)"); + + // WebRTC initialization commented out - not needed for Xpra + /* try { Gst.init(new Version(1, 14)); } @@ -71,43 +70,62 @@ public PulseAudioStreamer(String cid, Path pulsesock) log.log(Level.SEVERE, "Initializing GStreamer failed!", error); throw error; } + */ + + this.outqueue = new ArrayBlockingQueue<>(16); - this.outqueue = new ArrayBlockingQueue<>(16); + // WebRTC pipeline creation commented out - Xpra handles audio natively + /* this.pipeline = PulseAudioStreamer.createPipeline(log); this.audio = PulseAudioStreamer.createAudioBin(pulsesock.toString()); this.webrtc= PulseAudioStreamer.createWebRtcBin(pipeline, outqueue, log); - this.closed = false; + */ + this.closed = false; + this.playing = false; + + // WebRTC pipeline setup commented out - not needed for Xpra + /* pipeline.addMany(webrtc, audio); audio.link(webrtc); - } - - @Override - public String pollServerControlMessage(long timeout, TimeUnit unit) - { + */ + + log.info("PulseAudioStreamer initialized for Xpra native audio at: " + pulsesock); + } + + @Override + public String pollServerControlMessage(long timeout, TimeUnit unit) { + // WebRTC control messages not needed for Xpra native audio + // Return null to indicate no WebRTC signaling messages + log.fine("pollServerControlMessage called - WebRTC disabled, returning null for Xpra"); + return null; + + /* WebRTC implementation commented out: try { return outqueue.poll(timeout, unit); } catch (InterruptedException error) { return null; // Ignore it! } - } - - @Override - public void postClientControlMessage(char[] payload) throws IllegalArgumentException - { - this.postClientControlMessage(payload, 0, payload.length); - } - - @Override - public void postClientControlMessage(char[] payload, int offset, int length) throws IllegalArgumentException - { - this.postClientControlMessage(new String(payload, offset, length)); - } - - @Override - public void postClientControlMessage(String payload) throws IllegalArgumentException - { + */ + } + + @Override + public void postClientControlMessage(char[] payload) throws IllegalArgumentException { + this.postClientControlMessage(payload, 0, payload.length); + } + + @Override + public void postClientControlMessage(char[] payload, int offset, int length) throws IllegalArgumentException { + this.postClientControlMessage(new String(payload, offset, length)); + } + + @Override + public void postClientControlMessage(String payload) throws IllegalArgumentException { + // WebRTC control message handling commented out - not needed for Xpra native audio + log.fine("postClientControlMessage called with WebRTC disabled, using Xpra: " + payload); + + /* WebRTC implementation commented out: final ControlMessage message = ControlMessage.parse(payload); final String msgtype = message.getType(); switch (msgtype) { @@ -128,29 +146,46 @@ public void postClientControlMessage(String payload) throws IllegalArgumentExcep default: throw new IllegalArgumentException("Unknown message type: " + msgtype); } - } + */ + } - @Override - public void play() - { - log.info("Starting audio streamer..."); + @Override + public void play() { + log.info("Starting PulseAudioStreamer for Xpra native audio..."); + playing = true; + + // WebRTC pipeline start commented out - Xpra handles audio natively + /* pipeline.play(); - } + */ - @Override - public void stop() - { - log.info("Stopping audio streamer..."); + log.info("PulseAudioStreamer started - audio handled natively by Xpra"); + } + + @Override + public void stop() { + log.info("Stopping PulseAudioStreamer..."); + playing = false; + + // WebRTC pipeline stop commented out - not needed for Xpra + /* pipeline.stop(); final ControlMessage eosmessage = ControlMessage.wrap(new EosData()); outqueue.offer(eosmessage.toString()); - } + */ - @Override - public void close() - { - log.info("Closing audio streamer..."); + log.info("PulseAudioStreamer stopped - Xpra native audio"); + } + + @Override + public void close() { + log.info("Closing PulseAudioStreamer..."); + playing = false; + closed = true; + + // WebRTC cleanup commented out - not needed for Xpra + /* try { audio.unlink(webrtc); pipeline.remove(webrtc); @@ -164,17 +199,29 @@ public void close() finally { Gst.quit(); } - } + */ - @Override - public boolean isClosed() - { - return closed; - } + log.info("PulseAudioStreamer closed - Xpra native audio"); + } + + @Override + public boolean isClosed() { + return closed; + } + public boolean isPlaying() { + return playing; + } - // ========== Internal Helpers ============================== + public Path getPulseSocketPath() { + return pulsesock; + } + + // ========== WebRTC Internal Helpers - ALL COMMENTED OUT ============================== + + /* WebRTC pipeline creation commented out - not needed for Xpra: + private static Pipeline createPipeline(Logger log) { final Bus.STATE_CHANGED onStateChanged = (source, old, current, pending) -> { @@ -285,9 +332,15 @@ private static Bin createAudioBin(String pulsesock) return Gst.parseBinFromDescription(description, true); } + + */ } +// ========== WebRTC Control Message Classes - ALL COMMENTED OUT ============================== + +/* WebRTC control message classes commented out - not needed for Xpra: + interface JsonSerializable { JsonObject toJson(); @@ -526,3 +579,5 @@ public String toString() return this.toJson().toString(); } } + +*/ diff --git a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/XpraUtils.java b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/XpraUtils.java index 1f3f5e5..15bdab1 100755 --- a/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/XpraUtils.java +++ b/emucomp-impl/src/main/java/de/bwl/bwfla/emucomp/xpra/XpraUtils.java @@ -11,6 +11,22 @@ public class XpraUtils { + public static int allocateXpraPort() { + final Config config = ConfigProvider.getConfig(); + final int xpraPort = config.getValue("components.xpra.ports", Integer.class); + + try { + if (!XpraUtils.isReachable("localhost", xpraPort)) { + return xpraPort; + } + } catch (IOException e) { + // Port is not reachable, so it's available + return xpraPort; + } + + throw new RuntimeException("Xpra port " + xpraPort + " is already in use"); + } + public static boolean startXpraSession(ProcessRunner runner, String command, int port, Logger log) throws IOException { final Config config = ConfigProvider.getConfig(); @@ -21,26 +37,29 @@ public static boolean startXpraSession(ProcessRunner runner, String command, int runner.addArgument("--bind-tcp=localhost:" + port); runner.addArgument("--daemon=no"); runner.addArgument("--html=on"); + + runner.addArgument("--speaker=on"); + + // PulseAudio + runner.addArgument("--pulseaudio=yes"); + runner.addArgument("--pulseaudio-server=unix:/tmp/" + port + "/pulse-socket"); + runner.addArgument("--speaker-codec=opus"); + runner.addArgument("--audio-source=pulse"); + + runner.addEnvVariable("PULSE_RUNTIME_PATH", "/tmp/" + port); + runner.addEnvVariable("PULSE_STATE_PATH", "/tmp/" + port); + runner.addArgument("--start-child="); if (isGpuEnabled) runner.addArgValue("vglrun "); - runner.addArgValue(command); - // temporary hotfix - runner.addEnvVariable("XDG_RUNTIME_DIR", "/tmp/" + port); - return runner.start(); - } - public static boolean startXpraSession(ProcessRunner runner, int port, Logger log) { - runner.setCommand("xpra"); - runner.addArgument("start"); - runner.addArgument(":" + port); - runner.addArgument("--socket-dir=/tmp"); - runner.addArgument("--socket-dirs=/tmp"); - runner.addArgument("--bind-tcp=localhost:" + port); - runner.addArgument("--daemon=no"); - runner.addArgument("--html=on"); + if (command != null && !command.isBlank()) { + runner.addArgValue(command); + } + // temporary hotfix runner.addEnvVariable("XDG_RUNTIME_DIR", "/tmp/" + port); + return runner.start(); } diff --git a/emucomp-impl/src/main/resources/config/test-machine.json b/emucomp-impl/src/main/resources/config/test-machine.json index 442cf29..ed389dc 100755 --- a/emucomp-impl/src/main/resources/config/test-machine.json +++ b/emucomp-impl/src/main/resources/config/test-machine.json @@ -13,7 +13,7 @@ "clientKbdLayout": "us", "required": false }, - "audio_system": "webRTC", + "audio_system": "xpra", "disableGhostCursor": false }, "operatingSystemId": "os:linux:ubuntu", diff --git a/emucomp-impl/src/main/resources/default.yml b/emucomp-impl/src/main/resources/default.yml index 121bcf6..17f7dd3 100755 --- a/emucomp-impl/src/main/resources/default.yml +++ b/emucomp-impl/src/main/resources/default.yml @@ -27,7 +27,7 @@ components: runc: ${COMPONENTS_BINARY_RUNC:runc} vdeplug: ${COMPONENTS_BINARY_VDEPLUG:/usr/bin/vde_plug} xpra: - ports: ${COMPONENTS_XPRA_PORTS:7000-7100} + ports: ${COMPONENTS_XPRA_PORTS:7000} enable_gpu: ${COMPONENTS_XPRA_ENABLE_GPU:false} vice_defaults_dir: ${COMPONENTS_VICE_DEFAULTS_DIR:/home/bwfla/server-data/vice-defaults} emulator_containers: @@ -49,7 +49,7 @@ components: emucomp: blobstore_soap: ${EMUCOMP_BLOBSTORE_SOAP:http://eaas:8080} inactivitytimeout: ${EMUCOMP_INACTIVITY_TIMEOUT:0} - enable_pulseaudio: ${EMUCOMP_ENABLE_PULSEAUDIO:false} + enable_pulseaudio: ${EMUCOMP_ENABLE_PULSEAUDIO:true} enable_screenshooter: ${EMUCOMP_ENABLE_SCREENSHOOTER:false} debug_bean_enabled: ${EMUCOMP_DEBUG_BEAN_ENABLED:false} libfaketime: ${EMUCOMP_LIBFAKETIME:/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1} diff --git a/k8s/deployment.yml b/k8s/deployment.yml index 75ec7a5..f532787 100644 --- a/k8s/deployment.yml +++ b/k8s/deployment.yml @@ -18,10 +18,7 @@ spec: image: emucomp:latest imagePullPolicy: IfNotPresent ports: - - containerPort: 8080 # HTTP / Guacamole - - containerPort: 9000 # GRPC - - containerPort: 7000 # WebRTC start - - containerPort: 7015 # WebRTC end + - containerPort: 8080 --- apiVersion: batch/v1 kind: Job @@ -38,7 +35,4 @@ spec: image: emucomp:latest ports: - containerPort: 8080 - - containerPort: 9000 - - containerPort: 7000 - - containerPort: 7015 restartPolicy: Never \ No newline at end of file diff --git a/k8s/emucomp-gateway-service.yml b/k8s/emucomp-gateway-service.yml new file mode 100644 index 0000000..d145f4b --- /dev/null +++ b/k8s/emucomp-gateway-service.yml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: emucomp-service + namespace: default + labels: + app: emucomp + service: emucomp +spec: + type: ClusterIP + selector: + app: emucomp + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP \ No newline at end of file diff --git a/k8s/gateway.yml b/k8s/gateway.yml new file mode 100644 index 0000000..f28a801 --- /dev/null +++ b/k8s/gateway.yml @@ -0,0 +1,154 @@ +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: emucomp-session-gateway + namespace: default +spec: + selector: + istio: ingressgateway + servers: + - port: + number: 80 + name: http-session + protocol: HTTP + hosts: + - "session.emucomp.local" + - port: + number: 443 + name: https-session + protocol: HTTPS + tls: + mode: SIMPLE + credentialName: emucomp-session-tls-secret + hosts: + - "session.emucomp.local" +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: emucomp-session-vs + namespace: default +spec: + hosts: + - "session.emucomp.local" + gateways: + - emucomp-session-gateway + http: + - match: + - uri: + prefix: "/components/" + - headers: + upgrade: + exact: "websocket" + route: + - destination: + host: emucomp-service + port: + number: 8080 + timeout: 0s + websocketUpgrade: true + + - match: + - uri: + regex: "^/components/[^/]+/xpra$" + route: + - destination: + host: emucomp-service + port: + number: 8080 + timeout: 0s + websocketUpgrade: true + + - match: + - uri: + regex: "^/components/[^/]+/websocket$" + route: + - destination: + host: emucomp-service + port: + number: 8080 + timeout: 0s + websocketUpgrade: true + + - match: + - uri: + regex: "^/components/[^/]+/webemulator$" + route: + - destination: + host: emucomp-service + port: + number: 8080 + timeout: 0s + websocketUpgrade: true + + - match: + - uri: + regex: "^/components/[^/]+/ws\\+ethernet/[^/]+$" + route: + - destination: + host: emucomp-service + port: + number: 8080 + timeout: 0s + websocketUpgrade: true + + - match: + - uri: + prefix: "/session/" + route: + - destination: + host: emucomp-service + port: + number: 8080 + timeout: 60s + + - match: + - uri: + prefix: "/session-recorder/" + - uri: + prefix: "/session-player/" + route: + - destination: + host: emucomp-service + port: + number: 8080 + timeout: 300s + + - match: + - uri: + prefix: "/ComponentService/" + route: + - destination: + host: emucomp-service + port: + number: 8080 + timeout: 120s +--- +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: emucomp-session-destination + namespace: default +spec: + host: emucomp-service + trafficPolicy: + connectionPool: + tcp: + maxConnections: 100 + keepAlive: + time: 7200s + timeout: 60s + interval: 60s + http: + http1MaxPendingRequests: 50 + http2MaxRequests: 100 + maxRequestsPerConnection: 1 + h2UpgradePolicy: UPGRADE + useClientProtocol: true + loadBalancer: + consistentHash: + httpHeaderName: "X-Component-ID" + outlierDetection: + consecutive5xxErrors: 10 + interval: 60s + baseEjectionTime: 60s \ No newline at end of file diff --git a/k8s/service.yml b/k8s/service.yml index 4c163bd..3cdd7f9 100644 --- a/k8s/service.yml +++ b/k8s/service.yml @@ -1,24 +1,13 @@ apiVersion: v1 kind: Service metadata: - name: emucomp-headless + name: emucomp-service spec: - clusterIP: None + type: ClusterIP selector: app: emucomp ports: - name: http port: 8080 targetPort: 8080 - - name: grpc - port: 9000 - targetPort: 9000 - - name: guacamole - port: 8080 - targetPort: 8080 - - name: webrtc-base - port: 7000 - targetPort: 7000 - - name: webrtc-max - port: 7015 - targetPort: 7015 \ No newline at end of file + protocol: TCP \ No newline at end of file