diff --git a/server/core/src/main/java/dev/slimevr/NetworkProtocol.java b/server/core/src/main/java/dev/slimevr/NetworkProtocol.java deleted file mode 100644 index 07366b1c81..0000000000 --- a/server/core/src/main/java/dev/slimevr/NetworkProtocol.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.slimevr; - -public enum NetworkProtocol { - OWO_LEGACY, - SLIMEVR_RAW, - SLIMEVR_FLATBUFFER, - SLIMEVR_WEBSOCKET -} diff --git a/server/core/src/main/java/dev/slimevr/bridge/BridgeThread.java b/server/core/src/main/java/dev/slimevr/bridge/BridgeThread.java deleted file mode 100644 index 52f7224241..0000000000 --- a/server/core/src/main/java/dev/slimevr/bridge/BridgeThread.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.slimevr.bridge; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - - -@Retention(value = RetentionPolicy.SOURCE) -public @interface BridgeThread { -} diff --git a/server/core/src/main/java/dev/slimevr/bridge/BridgeThread.kt b/server/core/src/main/java/dev/slimevr/bridge/BridgeThread.kt new file mode 100644 index 0000000000..13554b243e --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/bridge/BridgeThread.kt @@ -0,0 +1,4 @@ +package dev.slimevr.bridge + +@Retention(AnnotationRetention.SOURCE) +annotation class BridgeThread diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCProvisioningHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCProvisioningHandler.kt index b1cd0387f2..d478a3ec3a 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCProvisioningHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCProvisioningHandler.kt @@ -25,7 +25,7 @@ class RPCProvisioningHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : val req = messageHeader .message(StartWifiProvisioningRequest()) as StartWifiProvisioningRequest? if (req == null) return - this.api.server.provisioningHandler.start(req.ssid(), req.password(), req.port()) + this.api.server.provisioningHandler.start(req.ssid() ?: error("expected ssid"), req.password() ?: error("expected password"), req.port()) conn.context.useProvisioning = true } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt index e621390af3..41a6a7005d 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/serial/RPCSerialHandler.kt @@ -43,7 +43,7 @@ class RPCSerialHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) : Seria ) } - override fun onSerialLog(str: String, server: Boolean) { + override fun onSerialLog(str: String, fromServer: Boolean) { val fbb = FlatBufferBuilder(32) val logOffset = fbb.createString(str) diff --git a/server/core/src/main/java/dev/slimevr/serial/ProvisioningHandler.java b/server/core/src/main/java/dev/slimevr/serial/ProvisioningHandler.java deleted file mode 100644 index a17b17dda4..0000000000 --- a/server/core/src/main/java/dev/slimevr/serial/ProvisioningHandler.java +++ /dev/null @@ -1,250 +0,0 @@ -package dev.slimevr.serial; - -import dev.slimevr.VRServer; -import io.eiren.util.logging.LogManager; -import kotlin.text.Regex; -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.CopyOnWriteArrayList; - - -public class ProvisioningHandler implements SerialListener { - - private ProvisioningStatus provisioningStatus = ProvisioningStatus.NONE; - - private boolean isRunning = false; - private final List listeners = new CopyOnWriteArrayList<>(); - - private String ssid; - private String password; - - private String preferredPort; - - private final Timer provisioningTickTimer = new Timer("ProvisioningTickTimer"); - private long lastStatusChange = -1; - private byte connectRetries = 0; - private boolean hasLogs = false; - private final byte MAX_CONNECTION_RETRIES = 1; - private final VRServer vrServer; - - public ProvisioningHandler(VRServer vrServer) { - this.vrServer = vrServer; - vrServer.serialHandler.addListener(this); - this.provisioningTickTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - if (!isRunning || provisioningStatus == ProvisioningStatus.DONE) - return; - provisioningTick(); - } - }, 0, 1000); - } - - - public void start(String ssid, String password, String port) { - this.isRunning = true; - this.hasLogs = false; - this.ssid = ssid; - this.password = password; - this.preferredPort = port; - this.provisioningStatus = ProvisioningStatus.NONE; - this.connectRetries = 0; - } - - public void stop() { - this.isRunning = false; - this.hasLogs = false; - this.ssid = null; - this.password = null; - this.connectRetries = 0; - this.changeStatus(ProvisioningStatus.NONE); - this.vrServer.serialHandler.closeSerial(); - } - - public void initSerial(String port) { - this.provisioningStatus = ProvisioningStatus.SERIAL_INIT; - this.hasLogs = false; - - try { - boolean openResult = false; - if (port != null) - openResult = vrServer.serialHandler.openSerial(port, false); - else - openResult = vrServer.serialHandler.openSerial(null, true); - if (!openResult) - LogManager.info("[SerialHandler] Serial port wasn't open..."); - } catch (Exception e) { - LogManager.severe("[SerialHandler] Unable to open serial port", e); - } catch (Throwable e) { - LogManager - .severe("[SerialHandler] Using serial ports is not supported on this platform", e); - } - - } - - public void tryObtainMacAddress() { - this.changeStatus(ProvisioningStatus.OBTAINING_MAC_ADDRESS); - vrServer.serialHandler.infoRequest(); - } - - public void tryProvisioning() { - this.changeStatus(ProvisioningStatus.PROVISIONING); - vrServer.serialHandler.setWifi(this.ssid, this.password); - } - - - public void provisioningTick() { - if (this.provisioningStatus == ProvisioningStatus.OBTAINING_MAC_ADDRESS) - this.tryObtainMacAddress(); - - if ( - !hasLogs - && this.provisioningStatus == ProvisioningStatus.OBTAINING_MAC_ADDRESS - && System.currentTimeMillis() - this.lastStatusChange > 1_000 - ) { - this.changeStatus(ProvisioningStatus.NO_SERIAL_LOGS_ERROR); - return; - } - - if ( - this.provisioningStatus == ProvisioningStatus.SERIAL_INIT - && vrServer.serialHandler.getKnownPorts().findAny().isEmpty() - && System.currentTimeMillis() - this.lastStatusChange > 15_000 - ) { - this.changeStatus(ProvisioningStatus.NO_SERIAL_DEVICE_FOUND); - return; - } - - if ( - System.currentTimeMillis() - this.lastStatusChange - > this.provisioningStatus.getTimeout() - ) { - if ( - this.provisioningStatus == ProvisioningStatus.NONE - || this.provisioningStatus == ProvisioningStatus.SERIAL_INIT - ) - this.initSerial(this.preferredPort); - else if (this.provisioningStatus == ProvisioningStatus.CONNECTING) - this.changeStatus(ProvisioningStatus.CONNECTION_ERROR); - else if (this.provisioningStatus == ProvisioningStatus.LOOKING_FOR_SERVER) - this.changeStatus(ProvisioningStatus.COULD_NOT_FIND_SERVER); - else if (!this.provisioningStatus.isError()) { - this.changeStatus(ProvisioningStatus.CONNECTION_ERROR); // TIMEOUT - } - } - } - - - @Override - public void onSerialConnected(@NotNull SerialPort port) { - if (!isRunning) - return; - this.tryObtainMacAddress(); - } - - @Override - public void onSerialDisconnected() { - if (!isRunning) - return; - this.changeStatus(ProvisioningStatus.NONE); - this.connectRetries = 0; - } - - @Override - public void onSerialLog(@NotNull String str, boolean server) { - if (!isRunning) - return; - if (!server) { - this.hasLogs = true; - if (provisioningStatus == ProvisioningStatus.NO_SERIAL_LOGS_ERROR) { - // Recover the onboarding process if the user turned on the - // tracker afterward - this.changeStatus(ProvisioningStatus.OBTAINING_MAC_ADDRESS); - } - } - - if ( - provisioningStatus == ProvisioningStatus.OBTAINING_MAC_ADDRESS && str.contains("mac:") - ) { - var match = new Regex("mac: (?([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})), ") - .find(str, str.indexOf("mac:")); - - if (match != null) { - var b = match.getGroups().get(1); - if (b != null) { - vrServer.configManager.getVrConfig().addKnownDevice(b.getValue()); - vrServer.configManager.saveConfig(); - this.tryProvisioning(); - } - } - - } - - if ( - provisioningStatus == ProvisioningStatus.PROVISIONING - && str.contains("New wifi credentials set") - ) { - this.changeStatus(ProvisioningStatus.CONNECTING); - } - - if ( - provisioningStatus == ProvisioningStatus.CONNECTING - && (str.contains("Looking for the server") - || str.contains("Searching for the server")) - ) { - this.changeStatus(ProvisioningStatus.LOOKING_FOR_SERVER); - } - - if ( - provisioningStatus == ProvisioningStatus.LOOKING_FOR_SERVER - && str.contains("Handshake successful") - ) { - this.changeStatus(ProvisioningStatus.DONE); - } - - if ( - provisioningStatus == ProvisioningStatus.CONNECTING - && str.contains("Can't connect from any credentials") - ) { - if (++connectRetries >= MAX_CONNECTION_RETRIES) { - this.changeStatus(ProvisioningStatus.CONNECTION_ERROR); - } else { - this.vrServer.serialHandler.rebootRequest(); - } - } - } - - public void changeStatus(ProvisioningStatus status) { - if (this.provisioningStatus != status) { - this.lastStatusChange = System.currentTimeMillis(); - this.listeners - .forEach( - (l) -> l - .onProvisioningStatusChange(status, vrServer.serialHandler.getCurrentPort()) - ); - this.provisioningStatus = status; - } - } - - @Override - public void onNewSerialDevice(SerialPort port) { - if (!isRunning) - return; - this.initSerial(this.preferredPort); - } - - public void addListener(ProvisioningListener channel) { - this.listeners.add(channel); - } - - public void removeListener(ProvisioningListener l) { - listeners.removeIf(listener -> l == listener); - } - - @Override - public void onSerialDeviceDeleted(@NotNull SerialPort port) { - } -} diff --git a/server/core/src/main/java/dev/slimevr/serial/ProvisioningHandler.kt b/server/core/src/main/java/dev/slimevr/serial/ProvisioningHandler.kt new file mode 100644 index 0000000000..af7b2b5172 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/serial/ProvisioningHandler.kt @@ -0,0 +1,216 @@ +package dev.slimevr.serial + +import dev.slimevr.VRServer +import io.eiren.util.logging.LogManager +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList +import java.util.function.Consumer +import kotlin.concurrent.scheduleAtFixedRate + +class ProvisioningHandler(private val server: VRServer) : SerialListener { + private var provisioningStatus = ProvisioningStatus.NONE + + private var isRunning = false + private val listeners = CopyOnWriteArrayList() + + private var ssid: String? = null + private var password: String? = null + + private var preferredPort: String? = null + + private val provisioningTickTimer = Timer("ProvisioningTickTimer") + private var lastStatusChange: Long = -1 + private var connectRetries: Byte = 0 + private var hasLogs = false + + companion object { + const val MAX_CONNECTION_RETRIES: Byte = 1 + } + + init { + server.serialHandler.addListener(this) + provisioningTickTimer.scheduleAtFixedRate(0, 1000) { + if (isRunning && provisioningStatus != ProvisioningStatus.DONE) provisioningTick() + } + } + + fun start(ssid: String, password: String, port: String?) { + this.isRunning = true + this.hasLogs = false + this.ssid = ssid + this.password = password + this.preferredPort = port + this.provisioningStatus = ProvisioningStatus.NONE + this.connectRetries = 0 + } + + fun stop() { + this.isRunning = false + this.hasLogs = false + this.ssid = null + this.password = null + this.connectRetries = 0 + changeStatus(ProvisioningStatus.NONE) + server.serialHandler.closeSerial() + } + + fun initSerial(port: String?) { + provisioningStatus = ProvisioningStatus.SERIAL_INIT + hasLogs = false + + try { + val openResult = if (port != null) { + server.serialHandler.openSerial(port, false) + } else { + server.serialHandler.openSerial(null, true) + } + if (!openResult) { + LogManager.info("[SerialHandler] Serial port wasn't open...") + } + } catch (e: Exception) { + LogManager.severe("[SerialHandler] Unable to open serial port", e) + } catch (e: Throwable) { + LogManager.severe("[SerialHandler] Using serial ports is not supported on this platform", e) + } + } + + fun tryObtainMacAddress() { + changeStatus(ProvisioningStatus.OBTAINING_MAC_ADDRESS) + server.serialHandler.infoRequest() + } + + fun tryProvisioning() { + changeStatus(ProvisioningStatus.PROVISIONING) + server.serialHandler.setWifi(ssid!!, password!!) + } + + fun provisioningTick() { + if (provisioningStatus == ProvisioningStatus.OBTAINING_MAC_ADDRESS) tryObtainMacAddress() + + if (!hasLogs && provisioningStatus == ProvisioningStatus.OBTAINING_MAC_ADDRESS && System.currentTimeMillis() - lastStatusChange > 1000) { + changeStatus(ProvisioningStatus.NO_SERIAL_LOGS_ERROR) + return + } + + if (provisioningStatus == ProvisioningStatus.SERIAL_INIT && + server.serialHandler.knownPorts.findAny().isEmpty && + System.currentTimeMillis() - lastStatusChange > 15000 + ) { + changeStatus(ProvisioningStatus.NO_SERIAL_DEVICE_FOUND) + return + } + + if (System.currentTimeMillis() - lastStatusChange + > provisioningStatus.timeout + ) { + if (provisioningStatus == ProvisioningStatus.NONE || + provisioningStatus == ProvisioningStatus.SERIAL_INIT + ) { + initSerial(preferredPort) + } else if (provisioningStatus == ProvisioningStatus.CONNECTING) { + changeStatus(ProvisioningStatus.CONNECTION_ERROR) + } else if (provisioningStatus == ProvisioningStatus.LOOKING_FOR_SERVER) { + changeStatus( + ProvisioningStatus.COULD_NOT_FIND_SERVER, + ) + } else if (!provisioningStatus.isError) { + changeStatus(ProvisioningStatus.CONNECTION_ERROR) // TIMEOUT + } + } + } + + override fun onSerialConnected(port: SerialPort) { + if (!isRunning) return + tryObtainMacAddress() + } + + override fun onSerialDisconnected() { + if (!isRunning) return + changeStatus(ProvisioningStatus.NONE) + connectRetries = 0 + } + + override fun onSerialLog(str: String, fromServer: Boolean) { + if (!isRunning) return + if (!fromServer) { + hasLogs = true + if (provisioningStatus == ProvisioningStatus.NO_SERIAL_LOGS_ERROR) { + // Recover the onboarding process if the user turned on the + // tracker afterward + changeStatus(ProvisioningStatus.OBTAINING_MAC_ADDRESS) + } + } + + if (provisioningStatus == ProvisioningStatus.OBTAINING_MAC_ADDRESS && str.contains("mac:")) { + val match = Regex("mac: (?([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})), ") + .find(str, str.indexOf("mac:")) + + if (match != null) { + val b = match.groups[1] + if (b != null) { + server.configManager.vrConfig.addKnownDevice(b.value) + server.configManager.saveConfig() + tryProvisioning() + } + } + } + + if (provisioningStatus == ProvisioningStatus.PROVISIONING && + str.contains("New wifi credentials set") + ) { + changeStatus(ProvisioningStatus.CONNECTING) + } + + if (provisioningStatus == ProvisioningStatus.CONNECTING && + ( + str.contains("Looking for the server") || + str.contains("Searching for the server") + ) + ) { + changeStatus(ProvisioningStatus.LOOKING_FOR_SERVER) + } + + if (provisioningStatus == ProvisioningStatus.LOOKING_FOR_SERVER && + str.contains("Handshake successful") + ) { + changeStatus(ProvisioningStatus.DONE) + } + + if (provisioningStatus == ProvisioningStatus.CONNECTING && + str.contains("Can't connect from any credentials") + ) { + if (++connectRetries >= MAX_CONNECTION_RETRIES) { + changeStatus(ProvisioningStatus.CONNECTION_ERROR) + } else { + server.serialHandler.rebootRequest() + } + } + } + + fun changeStatus(status: ProvisioningStatus) { + if (provisioningStatus != status) { + lastStatusChange = System.currentTimeMillis() + listeners + .forEach { l -> + l.onProvisioningStatusChange(status, server.serialHandler.getCurrentPort()) + } + provisioningStatus = status + } + } + + override fun onNewSerialDevice(port: SerialPort) { + if (!isRunning) return + initSerial(preferredPort) + } + + fun addListener(channel: ProvisioningListener) { + listeners.add(channel) + } + + fun removeListener(l: ProvisioningListener) { + listeners.removeIf { listener: ProvisioningListener? -> l === listener } + } + + override fun onSerialDeviceDeleted(port: SerialPort) { + } +} diff --git a/server/core/src/main/java/dev/slimevr/serial/ProvisioningListener.java b/server/core/src/main/java/dev/slimevr/serial/ProvisioningListener.java deleted file mode 100644 index a1cf17a03d..0000000000 --- a/server/core/src/main/java/dev/slimevr/serial/ProvisioningListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.slimevr.serial; - -public interface ProvisioningListener { - - void onProvisioningStatusChange(ProvisioningStatus status, SerialPort port); -} diff --git a/server/core/src/main/java/dev/slimevr/serial/ProvisioningListener.kt b/server/core/src/main/java/dev/slimevr/serial/ProvisioningListener.kt new file mode 100644 index 0000000000..4708865c2d --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/serial/ProvisioningListener.kt @@ -0,0 +1,5 @@ +package dev.slimevr.serial + +interface ProvisioningListener { + fun onProvisioningStatusChange(status: ProvisioningStatus, port: SerialPort?) +} diff --git a/server/core/src/main/java/dev/slimevr/serial/SerialListener.kt b/server/core/src/main/java/dev/slimevr/serial/SerialListener.kt index fb60905341..15a0b34a04 100644 --- a/server/core/src/main/java/dev/slimevr/serial/SerialListener.kt +++ b/server/core/src/main/java/dev/slimevr/serial/SerialListener.kt @@ -24,8 +24,8 @@ interface SerialListener { fun onSerialConnected(port: SerialPort) fun onSerialDisconnected() - // var server indicates if the log is injected by the server (not an actual serial log) - fun onSerialLog(str: String, server: Boolean) + // fromServer indicates if the log is injected by the server (not an actual serial log) + fun onSerialLog(str: String, fromServer: Boolean) fun onNewSerialDevice(port: SerialPort) // This is called when the serial diver does not see the device anymore diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/NetworkProtocol.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/NetworkProtocol.kt new file mode 100644 index 0000000000..10502f34a3 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/NetworkProtocol.kt @@ -0,0 +1,8 @@ +package dev.slimevr.tracking.trackers.udp + +enum class NetworkProtocol { + OWO_LEGACY, + SLIMEVR_RAW, + SLIMEVR_FLATBUFFER, + SLIMEVR_WEBSOCKET, +} diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt index ea5c6d5ba1..bb6ffaf5ec 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt @@ -1,7 +1,6 @@ package dev.slimevr.tracking.trackers.udp import com.jme3.math.FastMath -import dev.slimevr.NetworkProtocol import dev.slimevr.VRServer import dev.slimevr.config.config import dev.slimevr.protocol.rpc.MAG_TIMEOUT diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt index bed58741c8..611e1ee7a9 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt @@ -1,6 +1,5 @@ package dev.slimevr.tracking.trackers.udp -import dev.slimevr.NetworkProtocol import dev.slimevr.VRServer import dev.slimevr.tracking.trackers.Device import dev.slimevr.tracking.trackers.Tracker diff --git a/server/core/src/main/java/dev/slimevr/util/ann/VRServerThread.java b/server/core/src/main/java/dev/slimevr/util/ann/VRServerThread.java deleted file mode 100644 index 37d6d7180a..0000000000 --- a/server/core/src/main/java/dev/slimevr/util/ann/VRServerThread.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.slimevr.util.ann; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - - -@Retention(value = RetentionPolicy.SOURCE) -public @interface VRServerThread { - -} diff --git a/server/core/src/main/java/dev/slimevr/util/ann/VRServerThread.kt b/server/core/src/main/java/dev/slimevr/util/ann/VRServerThread.kt new file mode 100644 index 0000000000..1be5f8c143 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/util/ann/VRServerThread.kt @@ -0,0 +1,4 @@ +package dev.slimevr.util.ann + +@Retention(AnnotationRetention.SOURCE) +annotation class VRServerThread diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.java b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.java deleted file mode 100644 index 0e3cf870e4..0000000000 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.java +++ /dev/null @@ -1,111 +0,0 @@ -package dev.slimevr.websocketapi; - -import dev.slimevr.VRServer; -import dev.slimevr.protocol.GenericConnection; -import dev.slimevr.protocol.ProtocolAPI; -import dev.slimevr.protocol.ProtocolAPIServer; -import io.eiren.util.logging.LogManager; -import org.java_websocket.WebSocket; -import org.java_websocket.drafts.Draft_6455; -import org.java_websocket.handshake.ClientHandshake; -import org.java_websocket.server.WebSocketServer; -import org.jetbrains.annotations.NotNull; - -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.Objects; -import java.util.stream.Stream; - - -public class WebsocketAPI extends WebSocketServer implements ProtocolAPIServer { - - public final VRServer server; - public final ProtocolAPI protocolAPI; - - public WebsocketAPI(VRServer server, ProtocolAPI protocolAPI) { - super(new InetSocketAddress(21110), Collections.singletonList(new Draft_6455())); - this.server = server; - this.protocolAPI = protocolAPI; - - this.protocolAPI.registerAPIServer(this); - setReuseAddr(true); - } - - @Override - public void onOpen(WebSocket conn, ClientHandshake handshake) { - LogManager - .info( - "[WebSocketAPI] New connection from: " - + conn.getRemoteSocketAddress().getAddress().getHostAddress() - ); - conn.setAttachment(new WebsocketConnection(conn)); - } - - /** - * Helper function to get the string of the `conn` while handling `null` - */ - protected static String connAddr(WebSocket conn) { - if (conn == null) { - return "null"; - } - var remote = conn.getRemoteSocketAddress(); - if (remote == null) { - return conn.toString(); - } - var addr = remote.getAddress(); - if (addr == null) { - return remote.toString(); - } - return addr.getHostAddress(); - } - - @Override - public void onClose(WebSocket conn, int code, String reason, boolean remote) { - LogManager - .info( - "[WebSocketAPI] Disconnected: " - + connAddr(conn) - + ", (" - + code - + ") " - + reason - + ". Remote: " - + remote - ); - } - - @Override - public void onMessage(WebSocket conn, String message) { - } - - @Override - public void onMessage(WebSocket conn, ByteBuffer message) { - var connection = conn.getAttachment(); - if (connection != null) - this.protocolAPI.onMessage(connection, message); - } - - @Override - public void onError(WebSocket conn, Exception ex) { - LogManager - .severe( - "[WebSocket] Exception on connection " + connAddr(conn), - ex - ); - } - - @Override - public void onStart() { - LogManager.info("[WebSocketAPI] Web Socket API started on port " + getPort()); - setConnectionLostTimeout(0); - } - - @Override - public @NotNull Stream getApiConnections() { - return this.getConnections().stream().map(conn -> { - var c = conn.getAttachment(); - return (GenericConnection) c; - }).filter(Objects::nonNull); - } -} diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.kt b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.kt new file mode 100644 index 0000000000..06ef666053 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketAPI.kt @@ -0,0 +1,71 @@ +package dev.slimevr.websocketapi + +import dev.slimevr.VRServer +import dev.slimevr.protocol.GenericConnection +import dev.slimevr.protocol.ProtocolAPI +import dev.slimevr.protocol.ProtocolAPIServer +import io.eiren.util.logging.LogManager +import org.java_websocket.WebSocket +import org.java_websocket.drafts.Draft_6455 +import org.java_websocket.handshake.ClientHandshake +import org.java_websocket.server.WebSocketServer +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.util.stream.Stream + +open class WebsocketAPI(val server: VRServer, val protocolAPI: ProtocolAPI) : + WebSocketServer(InetSocketAddress(21110), mutableListOf(Draft_6455())), + ProtocolAPIServer { + init { + protocolAPI.registerAPIServer(this) + isReuseAddr = true + } + + override fun onOpen(conn: WebSocket, handshake: ClientHandshake) { + LogManager.info("[WebSocketAPI] New connection from ${conn.remoteSocketAddress.address.hostAddress}") + conn.setAttachment(WebsocketConnection(conn)) + } + + override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) { + LogManager.info("[WebSocketAPI] Disconnected: ${connAddr(conn)}, ($code) $reason. Remote: $remote") + } + + override fun onMessage(conn: WebSocket, message: String) { + } + + override fun onMessage(conn: WebSocket, message: ByteBuffer) { + val connection = conn.getAttachment() ?: return + protocolAPI.onMessage(connection, message) + } + + override fun onError(conn: WebSocket, ex: Exception) { + LogManager + .severe("[WebSocket] Exception on connection ${connAddr(conn)}", ex) + } + + override fun onStart() { + LogManager.info("[WebSocketAPI] Started on port ${getPort()}") + connectionLostTimeout = 0 + } + + override val apiConnections: Stream + get() = connections + .stream() + .map { conn: WebSocket -> + conn.getAttachment() as GenericConnection? + } + .filter { it != null } + .map { it!! } + + companion object { + /** + * Helper function to get the string of the `conn` while handling `null` + */ + protected fun connAddr(conn: WebSocket?): String { + if (conn == null) return "null" + + val remote = conn.remoteSocketAddress ?: return conn.toString() + return remote.address?.hostAddress ?: return remote.toString() + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java deleted file mode 100644 index 73340b7443..0000000000 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.slimevr.websocketapi; - -import dev.slimevr.protocol.ConnectionContext; -import dev.slimevr.protocol.GenericConnection; -import org.java_websocket.WebSocket; -import org.java_websocket.exceptions.WebsocketNotConnectedException; - -import java.nio.ByteBuffer; -import java.util.UUID; - - -public class WebsocketConnection implements GenericConnection { - - public final ConnectionContext context; - public final WebSocket conn; - public UUID id; - - public WebsocketConnection(WebSocket conn) { - this.context = new ConnectionContext(); - this.conn = conn; - this.id = UUID.randomUUID(); - } - - @Override - public ConnectionContext getContext() { - return this.context; - } - - @Override - public void send(ByteBuffer bytes) { - if (this.conn.isOpen()) { - try { - this.conn.send(bytes.slice()); - } catch (WebsocketNotConnectedException ignored) { - // Race condition if it closes between our check and sending - } - } - } - - @Override - public UUID getConnectionId() { - return id; - } -} diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.kt b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.kt new file mode 100644 index 0000000000..593902f31c --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.kt @@ -0,0 +1,23 @@ +package dev.slimevr.websocketapi + +import dev.slimevr.protocol.ConnectionContext +import dev.slimevr.protocol.GenericConnection +import org.java_websocket.WebSocket +import org.java_websocket.exceptions.WebsocketNotConnectedException +import java.nio.ByteBuffer +import java.util.* + +class WebsocketConnection(val conn: WebSocket) : GenericConnection { + override val context = ConnectionContext() + override val connectionId: UUID = UUID.randomUUID() + + override fun send(bytes: ByteBuffer) { + if (this.conn.isOpen) { + try { + this.conn.send(bytes.slice()) + } catch (ignored: WebsocketNotConnectedException) { + // Race condition if it closes between our check and sending + } + } + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt index 22267c260a..88c476d41f 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt @@ -235,7 +235,6 @@ fun provideBridges( server, Paths.get(OperatingSystem.socketDirectory, "SlimeVRRpc") .toString(), - computedTrackers, ), ) } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt index fdbee06725..0d7eb55896 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/ProtobufBridge.kt @@ -44,7 +44,7 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte protected abstract fun signalSend() @BridgeThread - protected abstract fun sendMessageReal(message: ProtobufMessage?): Boolean + protected abstract fun sendMessageReal(message: ProtobufMessage): Boolean private var remoteProtocolVersion: Int = 0 @@ -63,7 +63,7 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte protected fun updateMessageQueue() { var message: ProtobufMessage? while ((outputQueue.poll().also { message = it }) != null) { - if (!sendMessageReal(message)) return + if (!sendMessageReal(message!!)) return } } @@ -200,12 +200,12 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte } tracker = createNewTracker(trackerAdded) synchronized(remoteTrackersBySerial) { - remoteTrackersBySerial.put(tracker!!.name, tracker) + remoteTrackersBySerial.put(tracker.name, tracker) } synchronized(remoteTrackersByTrackerId) { - remoteTrackersByTrackerId.put(tracker!!.trackerNum, tracker) + remoteTrackersByTrackerId.put(tracker.trackerNum, tracker) } - instance.registerTracker(tracker!!) + instance.registerTracker(tracker) } @VRServerThread diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/SocketUtils.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/SocketUtils.java deleted file mode 100644 index f36af175cb..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/SocketUtils.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.slimevr.desktop.platform.linux; - -import java.io.IOException; -import java.net.StandardProtocolFamily; -import java.net.UnixDomainSocketAddress; -import java.nio.channels.SocketChannel; - - -public class SocketUtils { - - static boolean isSocketInUse(String socketPath) { - try (SocketChannel testChannel = SocketChannel.open(StandardProtocolFamily.UNIX)) { - testChannel.connect(UnixDomainSocketAddress.of(socketPath)); - return true; - } catch (IOException e) { - return false; - } - } - -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/SocketUtils.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/SocketUtils.kt new file mode 100644 index 0000000000..e45751681e --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/SocketUtils.kt @@ -0,0 +1,17 @@ +package dev.slimevr.desktop.platform.linux + +import java.io.IOException +import java.net.StandardProtocolFamily +import java.net.UnixDomainSocketAddress +import java.nio.channels.SocketChannel + +object SocketUtils { + fun isSocketInUse(socketPath: String) = try { + SocketChannel.open(StandardProtocolFamily.UNIX).use { testChannel -> + testChannel.connect(UnixDomainSocketAddress.of(socketPath)) + true + } + } catch (_: IOException) { + false + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java deleted file mode 100644 index 1c29cedd29..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java +++ /dev/null @@ -1,263 +0,0 @@ -package dev.slimevr.desktop.platform.linux; - -import com.google.protobuf.InvalidProtocolBufferException; -import dev.slimevr.VRServer; -import dev.slimevr.bridge.BridgeThread; -import dev.slimevr.desktop.platform.ProtobufMessages; -import dev.slimevr.desktop.platform.SteamVRBridge; -import dev.slimevr.tracking.trackers.Tracker; -import io.eiren.util.ann.ThreadSafe; -import io.eiren.util.logging.LogManager; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.io.IOException; -import java.net.StandardProtocolFamily; -import java.net.UnixDomainSocketAddress; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.nio.channels.Selector; -import java.nio.channels.SelectionKey; -import java.util.List; - - -public class UnixSocketBridge extends SteamVRBridge implements AutoCloseable { - public final String socketPath; - public final UnixDomainSocketAddress socketAddress; - private final String bridgeSettingsKey; - private final ByteBuffer dst = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN); - private final ByteBuffer src = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN); - - private ServerSocketChannel server; - private SocketChannel channel; - private Selector selector; - private boolean socketError = false; - - public UnixSocketBridge( - VRServer server, - String bridgeSettingsKey, - String bridgeName, - String socketPath, - List shareableTrackers - ) { - super(server, "Named socket thread", bridgeName, bridgeSettingsKey, shareableTrackers); - this.bridgeSettingsKey = bridgeSettingsKey; - this.socketPath = socketPath; - this.socketAddress = UnixDomainSocketAddress.of(socketPath); - - File socketFile = new File(socketPath); - if (socketFile.exists()) { - if (SocketUtils.isSocketInUse(socketPath)) { - throw new RuntimeException( - socketPath + " socket is already in use by another process." - ); - } else { - LogManager.warning("[" + bridgeName + "] Cleaning up stale socket: " + socketPath); - if (!socketFile.delete()) { - throw new RuntimeException("Failed to delete stale socket: " + socketPath); - } - } - } - socketFile.deleteOnExit(); - } - - @Override - @BridgeThread - public void run() { - try { - this.server = createSocket(); - while (true) { - if (this.channel == null) { - reportDisconnected(); - this.selector = Selector.open(); - this.channel = server.accept(); - this.channel.configureBlocking(false); - this.channel.register(this.selector, SelectionKey.OP_READ); - if (this.channel == null) - continue; - VRServer.Companion.getInstance().queueTask(this::reconnected); - LogManager - .info( - "[" - + bridgeName - + "]" - + " Connected to " - + this.channel.getRemoteAddress().toString() - ); - } else { - if (this.socketError || !this.channel.isConnected()) { - this.resetChannel(); - continue; - } - try { - boolean updated = this.updateSocket(); - updateMessageQueue(); - if (updated) { - reportConnected(); - } else { - this.waitForData(10); - } - } catch (IOException ioError) { - this.resetChannel(); - ioError.printStackTrace(); - try { - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - } - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - @Override - @ThreadSafe - protected void signalSend() { - Selector selector = this.selector; - if (selector == null) { - return; - } - selector.wakeup(); - } - - @BridgeThread - private void waitForData(long timeoutMs) throws IOException { - this.selector.select(timeoutMs); - } - - @Override - @BridgeThread - protected boolean sendMessageReal(ProtobufMessages.ProtobufMessage message) { - if (this.channel != null) { - try { - int size = message.getSerializedSize() + 4; - this.src.putInt(size); - byte[] serialized = message.toByteArray(); - this.src.put(serialized); - this.src.flip(); - - while (this.src.hasRemaining()) { - channel.write(this.src); - } - - this.src.clear(); - return true; - } catch (IOException e) { - e.printStackTrace(); - } - } - return false; - } - - private boolean updateSocket() throws IOException { - int read = channel.read(dst); - if (read == -1) { - LogManager - .info( - "[" - + bridgeName - + "] Reached end-of-stream on connection of " - + this.channel.getRemoteAddress().toString() - ); - socketError = true; - return false; - } else if (read == 0) { - return false; - } - - boolean readAnything = false; - // if buffer has 4 bytes at least, we got the message size! - // processs all messages - while (dst.position() >= 4) { - int messageLength = dst.getInt(0); - if (messageLength > 1024) { // Overflow - LogManager - .severe( - "[" - + bridgeName - + "] Buffer overflow on socket. Message length: " - + messageLength - ); - socketError = true; - break; - } else if (dst.position() >= messageLength) { - // Parse the message (this reads the array directly from the - // dst, so we need to move position ourselves) - try { - var message = parseMessage(dst.array(), 4, messageLength - 4); - this.messageReceived(message); - } catch (InvalidProtocolBufferException e) { - LogManager.severe("Failed to read protocol message", e); - } - int originalpos = dst.position(); - dst.position(messageLength); - dst.compact(); - // move position after compacting - dst.position(originalpos - messageLength); - readAnything = true; - } else { - break; - } - } - return readAnything; - } - - private static ProtobufMessages.ProtobufMessage parseMessage( - byte[] data, - int offset, - int length - ) throws InvalidProtocolBufferException { - return ProtobufMessages.ProtobufMessage - .parser() - .parseFrom(data, offset, length); - } - - private void resetChannel() throws IOException { - LogManager - .info( - "[" - + bridgeName - + "] Disconnected from " - + this.channel.getRemoteAddress().toString() - ); - this.selector.close(); - this.selector = null; - this.channel.close(); - this.channel = null; - this.socketError = false; - this.dst.clear(); - VRServer.Companion.getInstance().queueTask(this::disconnected); - } - - private ServerSocketChannel createSocket() throws IOException { - ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX); - server.bind(this.socketAddress); - LogManager.info("[" + bridgeName + "] Socket " + this.socketPath + " created"); - return server; - } - - @Override - public void close() throws Exception { - if (this.server != null) { - this.server.close(); - } - } - - @Override - public boolean isConnected() { - return channel != null && channel.isConnected(); - } - - @NotNull - @Override - public String getBridgeConfigKey() { - return bridgeSettingsKey; - } -} - diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.kt new file mode 100644 index 0000000000..e87479c261 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.kt @@ -0,0 +1,216 @@ +package dev.slimevr.desktop.platform.linux + +import com.google.protobuf.InvalidProtocolBufferException +import dev.slimevr.VRServer +import dev.slimevr.bridge.BridgeThread +import dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage +import dev.slimevr.desktop.platform.SteamVRBridge +import dev.slimevr.tracking.trackers.Tracker +import io.eiren.util.ann.ThreadSafe +import io.eiren.util.logging.LogManager +import java.io.File +import java.io.IOException +import java.lang.AutoCloseable +import java.lang.Thread.currentThread +import java.lang.Thread.sleep +import java.net.StandardProtocolFamily +import java.net.UnixDomainSocketAddress +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.SelectionKey +import java.nio.channels.Selector +import java.nio.channels.ServerSocketChannel +import java.nio.channels.SocketChannel + +class UnixSocketBridge( + server: VRServer, + bridgeSettingsKey: String, + bridgeName: String, + val socketPath: String, + shareableTrackers: List, +) : SteamVRBridge(server, "Named socket thread", bridgeName, bridgeSettingsKey, shareableTrackers), + AutoCloseable { + val socketAddress: UnixDomainSocketAddress = UnixDomainSocketAddress.of(socketPath) + private val dst: ByteBuffer = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN) + private val src: ByteBuffer = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN) + + lateinit var serverSocket: ServerSocketChannel + private var channel: SocketChannel? = null + private var selector: Selector? = null + private var socketError = false + + init { + val socketFile = File(socketPath) + if (socketFile.exists()) { + if (SocketUtils.isSocketInUse(socketPath)) { + throw RuntimeException("$socketPath socket is already in use by another process") + } + + LogManager.warning("[$bridgeName] Cleaning up stale socket $socketPath") + if (!socketFile.delete()) { + throw RuntimeException("Failed to delete stale socket $socketPath") + } + } + socketFile.deleteOnExit() + } + + @BridgeThread + override fun run() { + try { + serverSocket = createSocket() + while (true) { + if (channel == null) { + reportDisconnected() + selector = Selector.open() + channel = serverSocket.accept() ?: continue + channel!!.configureBlocking(false) + channel!!.register(selector, SelectionKey.OP_READ) + server.queueTask { reconnected() } + LogManager.info("[$bridgeName] Connected to ${channel!!.remoteAddress}") + continue + } + + if (socketError || !channel!!.isConnected) { + resetChannel() + continue + } + + try { + val updated = updateSocket() + updateMessageQueue() + if (updated) { + reportConnected() + } else { + waitForData(10) + } + } catch (e: IOException) { + resetChannel() + LogManager.severe("[$bridgeName] Exception when processing packets from socket", e) + try { + sleep(10) + } catch (_: InterruptedException) { + currentThread().interrupt() + break + } + } + } + } catch (e: IOException) { + LogManager.severe("[$bridgeName] Exception in listen loop", e) + } + } + + @ThreadSafe + override fun signalSend() { + val selector = selector ?: return + selector.wakeup() + } + + @BridgeThread + @Throws(IOException::class) + private fun waitForData(timeoutMs: Long) { + selector!!.select(timeoutMs) + } + + @BridgeThread + override fun sendMessageReal(message: ProtobufMessage): Boolean { + channel?.let { channel -> + try { + val size = message.getSerializedSize() + 4 + src.putInt(size) + val serialized = message.toByteArray() + src.put(serialized) + src.flip() + + while (src.hasRemaining()) { + channel.write(src) + } + + src.clear() + return true + } catch (e: IOException) { + LogManager.severe("[$bridgeName] Exception when sending message", e) + } + } + return false + } + + @Throws(IOException::class) + private fun updateSocket(): Boolean { + val read = channel!!.read(dst) + if (read == -1) { + LogManager.info("[$bridgeName] Reached end-of-stream on connection of ${channel!!.remoteAddress}") + socketError = true + return false + } else if (read == 0) { + return false + } + + var readAnything = false + // if buffer has 4 bytes at least, we got the message size! + // process all messages + while (dst.position() >= 4) { + val messageLength = dst.getInt(0) + if (messageLength > 1024) { // Overflow + LogManager.severe("[$bridgeName] Buffer overflow on socket. Message length: $messageLength") + socketError = true + break + } else if (dst.position() >= messageLength) { + // Parse the message (this reads the array directly from the + // dst, so we need to move position ourselves) + try { + val message: ProtobufMessage = parseMessage(dst.array(), 4, messageLength - 4) + messageReceived(message) + } catch (e: InvalidProtocolBufferException) { + LogManager.severe("[$bridgeName] Failed to read protocol message", e) + } + val originalPos = dst.position() + dst.position(messageLength) + dst.compact() + // move position after compacting + dst.position(originalPos - messageLength) + readAnything = true + } else { + break + } + } + return readAnything + } + + @Throws(IOException::class) + private fun resetChannel() { + LogManager.info("[$bridgeName] Disconnected from ${channel!!.remoteAddress}") + selector?.close() + selector = null + channel?.close() + channel = null + socketError = false + dst.clear() + server.queueTask { disconnected() } + } + + @Throws(IOException::class) + private fun createSocket(): ServerSocketChannel { + val server = ServerSocketChannel.open(StandardProtocolFamily.UNIX) + server.bind(socketAddress) + LogManager.info("[$bridgeName] Socket $socketPath created") + return server + } + + @Throws(Exception::class) + override fun close() { + serverSocket.close() + } + + override fun isConnected() = channel != null && channel!!.isConnected + + override fun getBridgeConfigKey(): String = bridgeSettingsKey + + companion object { + @Throws(InvalidProtocolBufferException::class) + private fun parseMessage( + data: ByteArray?, + offset: Int, + length: Int, + ) = ProtobufMessage.parser().parseFrom(data, offset, length) + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java deleted file mode 100644 index 45ccb81b0b..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java +++ /dev/null @@ -1,114 +0,0 @@ -package dev.slimevr.desktop.platform.linux; - -import dev.slimevr.protocol.ConnectionContext; -import dev.slimevr.protocol.GenericConnection; -import io.eiren.util.logging.LogManager; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.channels.SocketChannel; -import java.util.UUID; - - -public class UnixSocketConnection implements GenericConnection { - public final UUID id; - public final ConnectionContext context; - private final ByteBuffer dst = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN); - private final SocketChannel channel; - private int remainingBytes; - - public UnixSocketConnection(SocketChannel channel) { - this.id = UUID.randomUUID(); - this.context = new ConnectionContext(); - this.channel = channel; - } - - @Override - public UUID getConnectionId() { - return id; - } - - @Override - public ConnectionContext getContext() { - return this.context; - } - - public boolean isConnected() { - return this.channel.isConnected(); - } - - private void resetChannel() { - try { - this.channel.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void send(ByteBuffer bytes) { - if (!this.channel.isConnected()) - return; - try { - ByteBuffer[] src = new ByteBuffer[] { - ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN), - bytes.slice(), - }; - src[0].putInt(src[1].remaining() + 4); - src[0].flip(); - synchronized (this) { - while (src[1].hasRemaining()) { - this.channel.write(src); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - public ByteBuffer read() { - if (dst.position() < 4 || dst.position() < dst.getInt(0)) { - if (!this.channel.isConnected()) - return null; - try { - int result = this.channel.read(dst); - if (result == -1) { - LogManager.info("[SolarXR Bridge] Reached end-of-stream on connection"); - this.resetChannel(); - return null; - } - if (dst.position() < 4) { - return null; - } - } catch (IOException e) { - e.printStackTrace(); - this.resetChannel(); - return null; - } - } - int messageLength = dst.getInt(0); - if (messageLength > 1024) { - LogManager - .severe( - "[SolarXR Bridge] Buffer overflow on socket. Message length: " + messageLength - ); - this.resetChannel(); - return null; - } - if (dst.position() < messageLength) { - return null; - } - remainingBytes = dst.position() - messageLength; - dst.position(4); - dst.limit(messageLength); - return dst; - } - - public void next() { - dst.position(dst.limit()); - dst.limit(dst.limit() + remainingBytes); - dst.compact(); - dst.limit(dst.capacity()); - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.kt new file mode 100644 index 0000000000..dacf0042a7 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.kt @@ -0,0 +1,85 @@ +package dev.slimevr.desktop.platform.linux + +import dev.slimevr.protocol.ConnectionContext +import dev.slimevr.protocol.GenericConnection +import io.eiren.util.logging.LogManager +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.SocketChannel +import java.util.* + +class UnixSocketConnection(private val channel: SocketChannel) : GenericConnection { + override val connectionId: UUID = UUID.randomUUID() + override val context = ConnectionContext() + private val dst: ByteBuffer = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN) + private var remainingBytes = 0 + + private fun resetChannel() { + try { + channel.close() + } catch (e: IOException) { + LogManager.severe("[SolarXR Bridge] Failed to close socket", e) + } + } + + override fun send(bytes: ByteBuffer) { + if (!this.channel.isConnected) return + try { + val src = arrayOf( + ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN), + bytes.slice(), + ) + src[0].putInt(src[1].remaining() + 4) + src[0].flip() + synchronized(this) { + while (src[1].hasRemaining()) { + this.channel.write(src) + } + } + } catch (e: IOException) { + LogManager.severe("[SolarXR Bridge] Failed to send message", e) + } + } + + fun read(): ByteBuffer? { + if (dst.position() < 4 || dst.position() < dst.getInt(0)) { + if (!channel.isConnected) return null + try { + val result = channel.read(dst) + if (result == -1) { + LogManager.info("[SolarXR Bridge] Reached end-of-stream on connection") + resetChannel() + return null + } + if (dst.position() < 4) { + return null + } + } catch (e: IOException) { + LogManager.severe("[SolarXR Bridge] Exception when reading from connection", e) + resetChannel() + return null + } + } + val messageLength = dst.getInt(0) + if (messageLength > 1024) { + LogManager.severe("[SolarXR Bridge] Buffer overflow on socket. Message length: $messageLength") + resetChannel() + return null + } + if (dst.position() < messageLength) { + return null + } + remainingBytes = dst.position() - messageLength + dst.position(4) + dst.limit(messageLength) + return dst + } + + fun next() { + dst.position(dst.limit()) + dst.limit(dst.limit() + remainingBytes) + dst.compact() + dst.limit(dst.capacity()) + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java deleted file mode 100644 index 42dab98fbb..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java +++ /dev/null @@ -1,151 +0,0 @@ -package dev.slimevr.desktop.platform.linux; - -import dev.slimevr.bridge.BridgeThread; -import dev.slimevr.protocol.GenericConnection; -import dev.slimevr.protocol.ProtocolAPI; -import dev.slimevr.tracking.trackers.Tracker; -import dev.slimevr.util.ann.VRServerThread; -import dev.slimevr.VRServer; -import io.eiren.util.logging.LogManager; - -import java.io.File; -import java.io.IOException; -import java.net.StandardProtocolFamily; -import java.net.UnixDomainSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.util.List; - - -public class UnixSocketRpcBridge implements dev.slimevr.bridge.Bridge, - dev.slimevr.protocol.ProtocolAPIServer, Runnable, AutoCloseable { - private final Thread runnerThread = new Thread(this, "Named socket thread"); - private final String socketPath; - private final ProtocolAPI protocolAPI; - private final ServerSocketChannel socket; - private final Selector selector; - - public UnixSocketRpcBridge( - VRServer server, - String socketPath, - List shareableTrackers - ) { - this.socketPath = socketPath; - this.protocolAPI = server.protocolAPI; - - File socketFile = new File(socketPath); - if (socketFile.exists()) { - if (SocketUtils.isSocketInUse(socketPath)) { - throw new RuntimeException( - socketPath + " socket is already in use by another process." - ); - } else { - LogManager.warning("[SolarXR Bridge] Cleaning up stale socket: " + socketPath); - if (!socketFile.delete()) { - throw new RuntimeException("Failed to delete stale socket: " + socketPath); - } - } - } - socketFile.deleteOnExit(); - - try { - socket = ServerSocketChannel.open(StandardProtocolFamily.UNIX); - selector = Selector.open(); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException("Socket open failed."); - } - - server.protocolAPI.registerAPIServer(this); - } - - @VRServerThread - private void disconnected() { - } - - @Override - @VRServerThread - public void dataRead() { - } - - @Override - @VRServerThread - public void dataWrite() { - } - - @Override - @VRServerThread - public void addSharedTracker(Tracker tracker) { - } - - @Override - @VRServerThread - public void removeSharedTracker(Tracker tracker) { - } - - @Override - @VRServerThread - public void startBridge() { - this.runnerThread.start(); - } - - @Override - @BridgeThread - public void run() { - try { - this.socket.bind(UnixDomainSocketAddress.of(this.socketPath)); - this.socket.configureBlocking(false); - this.socket.register(this.selector, SelectionKey.OP_ACCEPT); - LogManager.info("[SolarXR Bridge] Socket " + this.socketPath + " created"); - while (this.socket.isOpen()) { - this.selector.select(0); - for (SelectionKey key : this.selector.selectedKeys()) { - UnixSocketConnection conn = (UnixSocketConnection) key.attachment(); - if (conn != null) { - for (ByteBuffer message; (message = conn.read()) != null; conn.next()) - this.protocolAPI.onMessage(conn, message); - } else - for (SocketChannel channel; (channel = socket.accept()) != null;) { - channel.configureBlocking(false); - channel - .register( - this.selector, - SelectionKey.OP_READ, - new UnixSocketConnection(channel) - ); - LogManager - .info( - "[SolarXR Bridge] Connected to " - + channel.getRemoteAddress().toString() - ); - } - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void close() throws Exception { - this.socket.close(); - this.selector.close(); - } - - @Override - public boolean isConnected() { - return this.selector.keys().stream().anyMatch(key -> key.attachment() != null); - } - - @Override - public java.util.stream.Stream getApiConnections() { - return this.selector - .keys() - .stream() - .map(key -> (GenericConnection) key.attachment()) - .filter(conn -> conn != null); - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.kt new file mode 100644 index 0000000000..d869381e16 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.kt @@ -0,0 +1,135 @@ +package dev.slimevr.desktop.platform.linux + +import dev.slimevr.VRServer +import dev.slimevr.bridge.Bridge +import dev.slimevr.bridge.BridgeThread +import dev.slimevr.protocol.GenericConnection +import dev.slimevr.protocol.ProtocolAPI +import dev.slimevr.protocol.ProtocolAPIServer +import dev.slimevr.tracking.trackers.Tracker +import dev.slimevr.util.ann.VRServerThread +import io.eiren.util.logging.LogManager +import java.io.File +import java.io.IOException +import java.lang.AutoCloseable +import java.net.StandardProtocolFamily +import java.net.UnixDomainSocketAddress +import java.nio.ByteBuffer +import java.nio.channels.SelectionKey +import java.nio.channels.Selector +import java.nio.channels.ServerSocketChannel +import java.nio.channels.SocketChannel +import java.util.* +import java.util.stream.Stream + +class UnixSocketRpcBridge( + private val server: VRServer, + socketPath: String, +) : Bridge, + ProtocolAPIServer, + Runnable, + AutoCloseable { + private val runnerThread = Thread(this, "Named socket thread") + private val socketAddress = UnixDomainSocketAddress.of(socketPath) + private val socket: ServerSocketChannel + private val selector: Selector + + init { + val socketFile = File(socketPath) + if (socketFile.exists()) { + if (SocketUtils.isSocketInUse(socketPath)) { + throw RuntimeException("$socketPath socket is already in use by another process") + } + + LogManager.warning("[SolarXR Bridge] Cleaning up stale socket $socketPath") + if (!socketFile.delete()) { + throw RuntimeException("Failed to delete stale socket $socketPath") + } + } + socketFile.deleteOnExit() + + try { + socket = ServerSocketChannel.open(StandardProtocolFamily.UNIX) + selector = Selector.open() + } catch (e: IOException) { + LogManager.severe("[SolarXR Bridge] Exception when opening socket $socketPath", e) + throw RuntimeException("Socket open failed.") + } + + server.protocolAPI.registerAPIServer(this) + } + + @VRServerThread + override fun dataRead() { + } + + @VRServerThread + override fun dataWrite() { + } + + @VRServerThread + override fun addSharedTracker(tracker: Tracker?) { + } + + @VRServerThread + override fun removeSharedTracker(tracker: Tracker?) { + } + + @VRServerThread + override fun startBridge() { + runnerThread.start() + } + + @BridgeThread + override fun run() { + try { + socket.bind(socketAddress) + socket.configureBlocking(false) + socket.register(selector, SelectionKey.OP_ACCEPT) + LogManager.info("[SolarXR Bridge] Socket $socketAddress created") + while (socket.isOpen) { + selector.select(0) + for (key in selector.selectedKeys()) { + val conn = key.attachment() as UnixSocketConnection? + if (conn != null) { + var message: ByteBuffer? + while ((conn.read().also { message = it }) != null) { + server.protocolAPI.onMessage(conn, message!!) + conn.next() + } + } else { + var channel: SocketChannel? + while ((socket.accept().also { channel = it }) != null) { + channel!!.configureBlocking(false) + channel + .register( + selector, + SelectionKey.OP_READ, + UnixSocketConnection(channel), + ) + LogManager.info("[SolarXR Bridge] Connected to ${channel.remoteAddress}") + } + } + } + } + } catch (e: IOException) { + LogManager.severe("[SolarXR Bridge] Exception when running bridge", e) + } + } + + @Throws(Exception::class) + override fun close() { + socket.close() + selector.close() + } + + override fun isConnected() = selector.keys().stream().anyMatch { key: SelectionKey? -> key!!.attachment() != null } + + override val apiConnections: Stream + get() = selector + .keys() + .stream() + .map { key: SelectionKey? -> key!!.attachment() as GenericConnection? } + .filter { obj: GenericConnection? -> Objects.nonNull(obj) } + .map { it!! } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/PipeState.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/PipeState.java deleted file mode 100644 index 81173b42d7..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/PipeState.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.slimevr.desktop.platform.windows; - -public enum PipeState { - CREATED, - OPEN, - ERROR -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java deleted file mode 100644 index 5099fccd8d..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java +++ /dev/null @@ -1,333 +0,0 @@ -package dev.slimevr.desktop.platform.windows; - -import com.google.protobuf.CodedOutputStream; -import com.google.protobuf.InvalidProtocolBufferException; -import com.sun.jna.Native; -import com.sun.jna.platform.win32.*; -import com.sun.jna.platform.win32.Kernel32; -import com.sun.jna.ptr.IntByReference; -import com.sun.jna.win32.W32APIOptions; -import dev.slimevr.VRServer; -import dev.slimevr.bridge.BridgeThread; -import dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage; -import dev.slimevr.desktop.platform.SteamVRBridge; -import dev.slimevr.tracking.trackers.Tracker; -import io.eiren.util.ann.ThreadSafe; -import io.eiren.util.logging.LogManager; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.util.List; - - -interface Kernel32IO extends Kernel32 { - Kernel32IO INSTANCE = Native.load("kernel32", Kernel32IO.class, W32APIOptions.DEFAULT_OPTIONS); - - boolean GetOverlappedResult( - /* [in] */ HANDLE hFile, - /* [in] */ WinBase.OVERLAPPED lpOverlapped, - /* [out] */ IntByReference lpNumberOfBytesTransferred, - /* [in] */ boolean bWait - ); -} - - -public class WindowsNamedPipeBridge extends SteamVRBridge { - private static final Kernel32 k32 = Kernel32.INSTANCE; - private static final Kernel32IO k32io = Kernel32IO.INSTANCE; - private static final Advapi32 adv32 = Advapi32.INSTANCE; - - protected final String pipeName; - protected final String bridgeSettingsKey; - private final byte[] buffArray = new byte[2048]; - protected WindowsPipe pipe; - protected WinNT.HANDLE openEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE readEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE writeEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE rxEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE txEvent = k32.CreateEvent(null, false, false, null); - protected WinNT.HANDLE[] events = new WinNT.HANDLE[] { rxEvent, txEvent }; - private final WinBase.OVERLAPPED overlappedOpen = new WinBase.OVERLAPPED(); - private final WinBase.OVERLAPPED overlappedWrite = new WinBase.OVERLAPPED(); - private final WinBase.OVERLAPPED overlappedRead = new WinBase.OVERLAPPED(); - private final WinBase.OVERLAPPED overlappedWait = new WinBase.OVERLAPPED(); - private final IntByReference bytesWritten = new IntByReference(0); - private final IntByReference bytesAvailable = new IntByReference(0); - private final IntByReference bytesRead = new IntByReference(0); - private boolean pendingWait = false; - - public WindowsNamedPipeBridge( - VRServer server, - String bridgeSettingsKey, - String bridgeName, - String pipeName, - List shareableTrackers - ) { - super(server, "Named pipe thread", bridgeName, bridgeSettingsKey, shareableTrackers); - this.pipeName = pipeName; - this.bridgeSettingsKey = bridgeSettingsKey; - overlappedWait.hEvent = rxEvent; - } - - @Override - @BridgeThread - public void run() { - try { - createPipe(); - while (true) { - boolean pipesUpdated = false; - if (pipe.state == PipeState.CREATED) { - // Report that our pipe is disconnected right now - reportDisconnected(); - tryOpeningPipe(pipe); - } - if (pipe.state == PipeState.OPEN) { - pipesUpdated = updatePipe(); - if (pipesUpdated) { - reportConnected(); - } - updateMessageQueue(); - } - if (pipe.state == PipeState.ERROR) { - resetPipe(); - } - if (!pipesUpdated) { - if (pipe.state == PipeState.OPEN) { - waitForData(10); - } else { - try { - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - } - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - @Override - @ThreadSafe - protected void signalSend() { - k32.SetEvent(txEvent); - } - - @BridgeThread - private void waitForData(int timeoutMs) { - if (pipe.state != PipeState.OPEN) - return; - if (!pendingWait) { - k32.ReadFile(pipe.pipeHandle, null, 0, null, overlappedWait); - pendingWait = true; - } - int evIdx = k32.WaitForMultipleObjects(events.length, events, false, timeoutMs); - if (evIdx == 0) { - // events[0] == overlappedWait.hEvent == rxEvent - pendingWait = false; - } - } - - @Override - @BridgeThread - protected boolean sendMessageReal(ProtobufMessage message) { - if (pipe.state != PipeState.OPEN) { - return false; - } - try { - int size = message.getSerializedSize(); - CodedOutputStream os = CodedOutputStream.newInstance(buffArray, 4, size); - message.writeTo(os); - size += 4; - buffArray[0] = (byte) (size & 0xFF); - buffArray[1] = (byte) ((size >> 8) & 0xFF); - buffArray[2] = (byte) ((size >> 16) & 0xFF); - buffArray[3] = (byte) ((size >> 24) & 0xFF); - - overlappedWrite.clear(); - overlappedWrite.hEvent = writeEvent; - boolean immediate = k32 - .WriteFile(pipe.pipeHandle, buffArray, size, null, overlappedWrite); - int err = k32.GetLastError(); - if (!immediate && err != WinError.ERROR_IO_PENDING) { - setPipeError("WriteFile failed: " + err); - return false; - } - - if (!k32io.GetOverlappedResult(pipe.pipeHandle, overlappedWrite, bytesWritten, true)) { - setPipeError( - "sendMessageReal/GetOverlappedResult failed: " + k32.GetLastError() - ); - return false; - } - - if (bytesWritten.getValue() != size) { - setPipeError("Bytes written " + bytesWritten.getValue() + ", expected " + size); - return false; - } - - return true; - } catch (IOException e) { - e.printStackTrace(); - } - return false; - } - - private boolean updatePipe() throws IOException { - if (pipe.state != PipeState.OPEN) { - return false; - } - boolean readAnything = false; - while (k32.PeekNamedPipe(pipe.pipeHandle, buffArray, 4, null, bytesAvailable, null)) { - if (bytesAvailable.getValue() < 4) { - return readAnything; // Wait for more data - } - int messageLength = (Byte.toUnsignedInt(buffArray[3]) << 24) - | (Byte.toUnsignedInt(buffArray[2]) << 16) - | (Byte.toUnsignedInt(buffArray[1]) << 8) - | Byte.toUnsignedInt(buffArray[0]); - if (messageLength > 1024) { // Overflow - setPipeError("Pipe overflow. Message length: " + messageLength); - return readAnything; - } - if (bytesAvailable.getValue() < messageLength) { - return readAnything; // Wait for more data - } - - overlappedRead.clear(); - overlappedRead.hEvent = readEvent; - boolean immediate = k32 - .ReadFile(pipe.pipeHandle, buffArray, messageLength, null, overlappedRead); - int err = k32.GetLastError(); - if (!immediate && err != WinError.ERROR_IO_PENDING) { - setPipeError("ReadFile failed: " + err); - return readAnything; - } - - if (!k32io.GetOverlappedResult(pipe.pipeHandle, overlappedRead, bytesRead, true)) { - setPipeError( - "updatePipe/GetOverlappedResult failed: " + k32.GetLastError() - ); - return readAnything; - } - - if (bytesRead.getValue() != messageLength) { - setPipeError( - "Bytes read " + bytesRead.getValue() + ", expected " + messageLength - ); - return readAnything; - } - - try { - ProtobufMessage message = ProtobufMessage - .parser() - .parseFrom(buffArray, 4, messageLength - 4); - messageReceived(message); - readAnything = true; - } catch (InvalidProtocolBufferException parseEx) { - parseEx.printStackTrace(); - setPipeError("Failed to parse message: " + parseEx.getMessage()); - return readAnything; - } - } - - int err = k32.GetLastError(); - if (err == WinError.ERROR_BROKEN_PIPE) { - setPipeError("Pipe closed"); - } else { - setPipeError("Pipe error: " + err); - } - return readAnything; - } - - private void setPipeError(String message) { - pipe.state = PipeState.ERROR; - LogManager.severe("[" + bridgeName + "] " + message); - } - - private void resetPipe() { - WindowsPipe.safeDisconnect(pipe); - pipe.state = PipeState.CREATED; - VRServer.Companion.getInstance().queueTask(this::disconnected); - } - - private void createPipe() throws IOException { - try { - WinNT.SECURITY_DESCRIPTOR descriptor = new WinNT.SECURITY_DESCRIPTOR(64 * 1024); - adv32.InitializeSecurityDescriptor(descriptor, WinNT.SECURITY_DESCRIPTOR_REVISION); - adv32.SetSecurityDescriptorDacl(descriptor, true, null, false); - adv32 - .SetSecurityDescriptorControl( - descriptor, - (short) WinNT.SE_DACL_PROTECTED, - (short) WinNT.SE_DACL_PROTECTED - ); - - WinBase.SECURITY_ATTRIBUTES attributes = new WinBase.SECURITY_ATTRIBUTES(); - attributes.lpSecurityDescriptor = descriptor.getPointer(); - attributes.bInheritHandle = false; - - pipe = new WindowsPipe( - k32 - .CreateNamedPipe( - pipeName, - WinBase.PIPE_ACCESS_DUPLEX | WinNT.FILE_FLAG_OVERLAPPED, // dwOpenMode - WinBase.PIPE_TYPE_BYTE | WinBase.PIPE_READMODE_BYTE | WinBase.PIPE_WAIT, // dwPipeMode - 1, // nMaxInstances, - 1024 * 16, // nOutBufferSize, - 1024 * 16, // nInBufferSize, - 0, // nDefaultTimeOut, - attributes // lpSecurityAttributes - ), - pipeName - ); - LogManager.info("[" + bridgeName + "] Pipe " + pipe.name + " created"); - if (WinBase.INVALID_HANDLE_VALUE.equals(pipe.pipeHandle)) { - throw new IOException("Can't open " + pipeName + " pipe: " + k32.GetLastError()); - } - LogManager.info("[" + bridgeName + "] Pipes are created"); - } catch (IOException e) { - WindowsPipe.safeDisconnect(pipe); - throw e; - } - } - - private boolean tryOpeningPipe(WindowsPipe pipe) { - overlappedOpen.clear(); - overlappedOpen.hEvent = openEvent; - - boolean ok = k32.ConnectNamedPipe(pipe.pipeHandle, overlappedOpen); - int err = k32.GetLastError(); - if (!ok && err != WinError.ERROR_PIPE_CONNECTED) { - if (err != WinError.ERROR_IO_PENDING) { - setPipeError("ConnectNamedPipe failed: " + err); - return false; - } - - if (!k32io.GetOverlappedResult(pipe.pipeHandle, overlappedOpen, bytesRead, true)) { - setPipeError( - "tryOpeningPipe/GetOverlappedResult failed: " + k32.GetLastError() - ); - return false; - } - } - - pipe.state = PipeState.OPEN; - LogManager.info("[" + bridgeName + "] Pipe " + pipe.name + " is open"); - VRServer.Companion.getInstance().queueTask(this::reconnected); - return true; - } - - @Override - public boolean isConnected() { - return pipe != null && pipe.state == PipeState.OPEN; - } - - @NotNull - @Override - public String getBridgeConfigKey() { - return this.bridgeSettingsKey; - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.kt new file mode 100644 index 0000000000..8bdb949223 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.kt @@ -0,0 +1,337 @@ +package dev.slimevr.desktop.platform.windows + +import com.google.protobuf.CodedOutputStream +import com.google.protobuf.InvalidProtocolBufferException +import com.sun.jna.Native +import com.sun.jna.platform.win32.Advapi32 +import com.sun.jna.platform.win32.Kernel32 +import com.sun.jna.platform.win32.WinBase +import com.sun.jna.platform.win32.WinError +import com.sun.jna.platform.win32.WinNT +import com.sun.jna.ptr.IntByReference +import com.sun.jna.win32.W32APIOptions +import dev.slimevr.VRServer +import dev.slimevr.bridge.BridgeThread +import dev.slimevr.desktop.platform.ProtobufMessages.ProtobufMessage +import dev.slimevr.desktop.platform.SteamVRBridge +import dev.slimevr.tracking.trackers.Tracker +import io.eiren.util.ann.ThreadSafe +import io.eiren.util.logging.LogManager +import java.io.IOException +import java.lang.Byte +import kotlin.Boolean +import kotlin.ByteArray +import kotlin.Int +import kotlin.String +import kotlin.Suppress +import kotlin.Throws +import kotlin.arrayOf + +internal interface Kernel32IO : Kernel32 { + @Suppress("FunctionName") + fun GetOverlappedResult( + hFile: WinNT.HANDLE?, /* [in] */ + lpOverlapped: WinBase.OVERLAPPED?, /* [in] */ + lpNumberOfBytesTransferred: IntByReference?, /* [out] */ + bWait: Boolean, /* [in] */ + ): Boolean + + companion object { + val INSTANCE: Kernel32IO = + Native.load("kernel32", Kernel32IO::class.java, W32APIOptions.DEFAULT_OPTIONS) + } +} + +enum class PipeState { + CREATED, + OPEN, + ERROR, +} +class WindowsPipe(val pipeHandle: WinNT.HANDLE?, val name: String) { + var state: PipeState = PipeState.CREATED + + fun safeDisconnect() { + if (pipeHandle != null && pipeHandle != WinBase.INVALID_HANDLE_VALUE) { + Kernel32.INSTANCE.DisconnectNamedPipe(pipeHandle) + } + } +} + +class WindowsNamedPipeBridge( + server: VRServer, + bridgeSettingsKey: String, + bridgeName: String, + private val pipeName: String, + shareableTrackers: List, +) : SteamVRBridge(server, "Named pipe thread", bridgeName, bridgeSettingsKey, shareableTrackers) { + lateinit var pipe: WindowsPipe + private val buffArray = ByteArray(2048) + private val openEvent: WinNT.HANDLE? = k32.CreateEvent(null, false, false, null) + private val readEvent: WinNT.HANDLE? = k32.CreateEvent(null, false, false, null) + private val writeEvent: WinNT.HANDLE? = k32.CreateEvent(null, false, false, null) + private val rxEvent: WinNT.HANDLE? = k32.CreateEvent(null, false, false, null) + private val txEvent: WinNT.HANDLE? = k32.CreateEvent(null, false, false, null) + private val events = arrayOf(rxEvent, txEvent) + private val overlappedOpen = WinBase.OVERLAPPED() + private val overlappedWrite = WinBase.OVERLAPPED() + private val overlappedRead = WinBase.OVERLAPPED() + private val overlappedWait = WinBase.OVERLAPPED() + private val bytesWritten = IntByReference(0) + private val bytesAvailable = IntByReference(0) + private val bytesRead = IntByReference(0) + private var pendingWait = false + + init { + overlappedWait.hEvent = rxEvent + } + + @BridgeThread + override fun run() { + try { + createPipe() + while (true) { + var pipesUpdated = false + if (pipe.state == PipeState.CREATED) { + // Report that our pipe is disconnected right now + reportDisconnected() + tryOpeningPipe(pipe) + } + if (pipe.state == PipeState.OPEN) { + pipesUpdated = updatePipe() + if (pipesUpdated) { + reportConnected() + } + updateMessageQueue() + } + if (pipe.state == PipeState.ERROR) { + resetPipe() + } + if (!pipesUpdated) { + if (pipe.state == PipeState.OPEN) { + waitForData(10) + } else { + try { + Thread.sleep(10) + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + } + } + } + } + } catch (e: IOException) { + LogManager.severe("[$bridgeName] Exception while running bridge", e) + } + } + + @ThreadSafe + override fun signalSend() { + k32.SetEvent(txEvent) + } + + @BridgeThread + private fun waitForData(timeoutMs: Int) { + if (pipe.state != PipeState.OPEN) return + if (!pendingWait) { + k32.ReadFile(pipe.pipeHandle, null, 0, null, overlappedWait) + pendingWait = true + } + val evIdx: Int = k32.WaitForMultipleObjects(events.size, events, false, timeoutMs) + if (evIdx == 0) { + // events[0] == overlappedWait.hEvent == rxEvent + pendingWait = false + } + } + + @BridgeThread + override fun sendMessageReal(message: ProtobufMessage): Boolean { + if (pipe.state != PipeState.OPEN) return false + try { + var size = message.getSerializedSize() + val os = CodedOutputStream.newInstance(buffArray, 4, size) + message.writeTo(os) + size += 4 + buffArray[0] = (size and 0xFF).toByte() + buffArray[1] = ((size shr 8) and 0xFF).toByte() + buffArray[2] = ((size shr 16) and 0xFF).toByte() + buffArray[3] = ((size shr 24) and 0xFF).toByte() + + overlappedWrite.clear() + overlappedWrite.hEvent = writeEvent + val immediate: Boolean = k32 + .WriteFile(pipe.pipeHandle, buffArray, size, null, overlappedWrite) + val err: Int = k32.GetLastError() + if (!immediate && err != WinError.ERROR_IO_PENDING) { + setPipeError("WriteFile failed: $err") + return false + } + + if (!k32io.GetOverlappedResult(pipe.pipeHandle, overlappedWrite, bytesWritten, true)) { + setPipeError("sendMessageReal/GetOverlappedResult failed: ${k32.GetLastError()}") + return false + } + + if (bytesWritten.value != size) { + setPipeError("Bytes written ${bytesWritten.value}, expected $size") + return false + } + + return true + } catch (e: IOException) { + LogManager.severe("[$bridgeName] Failed to send message", e) + } + return false + } + + @Throws(IOException::class) + private fun updatePipe(): Boolean { + if (pipe.state != PipeState.OPEN) { + return false + } + var readAnything = false + while (k32.PeekNamedPipe(pipe.pipeHandle, buffArray, 4, null, bytesAvailable, null)) { + if (bytesAvailable.value < 4) { + return readAnything // Wait for more data + } + val messageLength = ( + (Byte.toUnsignedInt(buffArray[3]) shl 24) + or (Byte.toUnsignedInt(buffArray[2]) shl 16) + or (Byte.toUnsignedInt(buffArray[1]) shl 8) + or Byte.toUnsignedInt(buffArray[0]) + ) + if (messageLength > 1024) { // Overflow + setPipeError("Pipe overflow. Message length: $messageLength") + return readAnything + } + if (bytesAvailable.value < messageLength) { + return readAnything // Wait for more data + } + + overlappedRead.clear() + overlappedRead.hEvent = readEvent + val immediate: Boolean = k32 + .ReadFile(pipe.pipeHandle, buffArray, messageLength, null, overlappedRead) + val err: Int = k32.GetLastError() + if (!immediate && err != WinError.ERROR_IO_PENDING) { + setPipeError("ReadFile failed: $err") + return readAnything + } + + if (!k32io.GetOverlappedResult(pipe.pipeHandle, overlappedRead, bytesRead, true)) { + setPipeError("updatePipe/GetOverlappedResult failed: ${k32.GetLastError()}") + return readAnything + } + + if (bytesRead.value != messageLength) { + setPipeError("Bytes read ${bytesRead.value}, expected $messageLength") + return readAnything + } + + try { + val message = ProtobufMessage + .parser() + .parseFrom(buffArray, 4, messageLength - 4) + messageReceived(message) + readAnything = true + } catch (e: InvalidProtocolBufferException) { + setPipeError("Failed to parse message: ${e.message}") + e.printStackTrace() + return readAnything + } + } + + val err = k32.GetLastError() + if (err == WinError.ERROR_BROKEN_PIPE) { + setPipeError("Pipe closed") + } else { + setPipeError("Pipe error: $err") + } + return readAnything + } + + private fun setPipeError(message: String) { + pipe.state = PipeState.ERROR + LogManager.severe("[$bridgeName] $message") + } + + private fun resetPipe() { + pipe.safeDisconnect() + pipe.state = PipeState.CREATED + server.queueTask { this.disconnected() } + } + + @Throws(IOException::class) + private fun createPipe() { + try { + val descriptor = WinNT.SECURITY_DESCRIPTOR(64 * 1024) + adv32.InitializeSecurityDescriptor(descriptor, WinNT.SECURITY_DESCRIPTOR_REVISION) + adv32.SetSecurityDescriptorDacl(descriptor, true, null, false) + adv32 + .SetSecurityDescriptorControl( + descriptor, + WinNT.SE_DACL_PROTECTED.toShort(), + WinNT.SE_DACL_PROTECTED.toShort(), + ) + + val attributes = WinBase.SECURITY_ATTRIBUTES() + attributes.lpSecurityDescriptor = descriptor.pointer + attributes.bInheritHandle = false + + pipe = WindowsPipe( + k32 + .CreateNamedPipe( + pipeName, + WinBase.PIPE_ACCESS_DUPLEX or WinNT.FILE_FLAG_OVERLAPPED, // dwOpenMode + WinBase.PIPE_TYPE_BYTE or WinBase.PIPE_READMODE_BYTE or WinBase.PIPE_WAIT, // dwPipeMode + 1, // nMaxInstances, + 1024 * 16, // nOutBufferSize, + 1024 * 16, // nInBufferSize, + 0, // nDefaultTimeOut, + attributes, // lpSecurityAttributes + ), + pipeName, + ) + LogManager.info("[$bridgeName] Pipe ${pipe.name} created") + if (WinBase.INVALID_HANDLE_VALUE == pipe.pipeHandle) { + throw IOException("Can't open $pipeName pipe: ${k32.GetLastError()}") + } + LogManager.info("[$bridgeName] Pipes are created") + } catch (e: IOException) { + pipe.safeDisconnect() + throw e + } + } + + private fun tryOpeningPipe(pipe: WindowsPipe): Boolean { + overlappedOpen.clear() + overlappedOpen.hEvent = openEvent + + val ok = k32.ConnectNamedPipe(pipe.pipeHandle, overlappedOpen) + val err = k32.GetLastError() + if (!ok && err != WinError.ERROR_PIPE_CONNECTED) { + if (err != WinError.ERROR_IO_PENDING) { + setPipeError("ConnectNamedPipe failed: $err") + return false + } + + if (!k32io.GetOverlappedResult(pipe.pipeHandle, overlappedOpen, bytesRead, true)) { + setPipeError("tryOpeningPipe/GetOverlappedResult failed: ${k32.GetLastError()}") + return false + } + } + + pipe.state = PipeState.OPEN + LogManager.info("[$bridgeName] Pipe ${pipe.name} is open") + server.queueTask { this.reconnected() } + return true + } + + override fun isConnected() = pipe.state == PipeState.OPEN + + override fun getBridgeConfigKey() = this.bridgeSettingsKey + + companion object { + private val k32: Kernel32 = Kernel32.INSTANCE + private val k32io: Kernel32IO = Kernel32IO.INSTANCE + private val adv32: Advapi32 = Advapi32.INSTANCE + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsPipe.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsPipe.java deleted file mode 100644 index 6aabd03adc..0000000000 --- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsPipe.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.slimevr.desktop.platform.windows; - -import com.sun.jna.platform.win32.Kernel32; -import com.sun.jna.platform.win32.WinNT.HANDLE; - - -public class WindowsPipe { - - public final String name; - public final HANDLE pipeHandle; - public PipeState state = PipeState.CREATED; - - public WindowsPipe(HANDLE pipeHandle, String name) { - this.pipeHandle = pipeHandle; - this.name = name; - } - - public static void safeDisconnect(WindowsPipe pipe) { - try { - if (pipe != null && pipe.pipeHandle != null) - Kernel32.INSTANCE.DisconnectNamedPipe(pipe.pipeHandle); - } catch (Exception ignored) {} - } -} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt b/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt index 1b911837be..223dde5047 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt @@ -104,10 +104,7 @@ class DesktopSerialHandler : } if (isConnected) { if (SerialPortWrapper(newPort) != currentPort?.let { SerialPortWrapper(it) }) { - LogManager.info( - "[SerialHandler] Closing current serial port " + - currentPort!!.descriptivePortName, - ) + LogManager.info("[SerialHandler] Closing current serial port ${currentPort!!.descriptivePortName}") currentPort!!.removeDataListener() currentPort!!.closePort() } else { @@ -117,18 +114,13 @@ class DesktopSerialHandler : } } currentPort = newPort - LogManager.info( - "[SerialHandler] Trying to connect to new serial port " + - currentPort!!.descriptivePortName, - ) - currentPort?.setBaudRate(115200) + LogManager.info("[SerialHandler] Trying to connect to new serial port ${currentPort!!.descriptivePortName}") + currentPort?.baudRate = 115200 currentPort?.clearRTS() currentPort?.clearDTR() if (currentPort?.openPort(1000) == false) { - LogManager.warning( - "[SerialHandler] Can't open serial port ${currentPort?.descriptivePortName}, last error: ${currentPort?.lastErrorCode}", - - ) + LogManager + .warning("[SerialHandler] Can't open serial port ${currentPort?.descriptivePortName}, last error: ${currentPort?.lastErrorCode}") currentPort = null return false } @@ -208,9 +200,9 @@ class DesktopSerialHandler : } } - fun addLog(str: String, server: Boolean = true) { + fun addLog(str: String, fromServer: Boolean = true) { LogManager.info("[Serial] $str") - listeners.forEach { it.onSerialLog(str, server) } + listeners.forEach { it.onSerialLog(str, fromServer) } } override fun getListeningEvents(): Int = (