diff --git a/docs/CALL_TUNNEL.md b/docs/CALL_TUNNEL.md index 8fd3c024..fb90806c 100644 --- a/docs/CALL_TUNNEL.md +++ b/docs/CALL_TUNNEL.md @@ -42,7 +42,7 @@ For each call, signal-cli: The `signal-call-tunnel` binary is located by searching (in order): 1. `SIGNAL_CALL_TUNNEL_BIN` environment variable -2. `/bin/signal-call-tunnel` +2. `/bin/signal-call-tunnel` (detected from jar location) 3. `signal-call-tunnel` on `PATH` ### Config JSON diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java index e7effe7b..a01bc6f8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java @@ -421,19 +421,35 @@ public class CallManager implements AutoCloseable { return envPath; } - // Check relative to the signal-cli installation - var installDir = System.getProperty("signal.cli.install.dir"); - if (installDir != null) { - var binPath = Path.of(installDir, "bin", "signal-call-tunnel"); - if (Files.isExecutable(binPath)) { - return binPath.toString(); + // Check relative to the signal-cli installation directory + try { + var codeSource = CallManager.class.getProtectionDomain().getCodeSource(); + if (codeSource != null) { + var jarPath = Path.of(codeSource.getLocation().toURI()); + var binPath = tunnelBinaryFromCodeSourcePath(jarPath); + if (Files.isExecutable(binPath)) { + return binPath.toString(); + } } + } catch (Exception e) { + logger.debug("Failed to determine install dir from code source", e); } // Fall back to PATH return "signal-call-tunnel"; } + /** + * Resolves the expected tunnel binary path from a code source path. + * The code source (jar or class dir) is expected to be in {@code /lib/}, + * so we go up two levels to reach the install root, then look for + * {@code bin/signal-call-tunnel}. + */ + static Path tunnelBinaryFromCodeSourcePath(Path codeSourcePath) { + var installDir = codeSourcePath.getParent().getParent(); + return installDir.resolve("bin").resolve("signal-call-tunnel"); + } + private String buildConfig(CallState state) { // Generate control channel authentication token var tokenBytes = new byte[32]; diff --git a/lib/src/test/java/org/asamk/signal/manager/helper/CallManagerTest.java b/lib/src/test/java/org/asamk/signal/manager/helper/CallManagerTest.java index 6e6a5156..6b77e286 100644 --- a/lib/src/test/java/org/asamk/signal/manager/helper/CallManagerTest.java +++ b/lib/src/test/java/org/asamk/signal/manager/helper/CallManagerTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.params.provider.ValueSource; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.math.BigInteger; import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -412,6 +413,38 @@ class CallManagerTest { assertEquals(null, info.outputDeviceName()); } + // ======================================================================== + // tunnelBinaryFromCodeSourcePath tests + // + // The install dir is derived from the code source location (jar or class + // directory): go up two levels (out of lib/) to reach the install root, + // then resolve bin/signal-call-tunnel. + // ======================================================================== + + @Test + void tunnelBinaryFromCodeSourcePath_resolvesFromJarInLib() { + // Simulate: /opt/signal-cli/lib/signal-cli.jar + var jarPath = Path.of("/opt/signal-cli/lib/signal-cli.jar"); + var result = CallManager.tunnelBinaryFromCodeSourcePath(jarPath); + assertEquals(Path.of("/opt/signal-cli/bin/signal-call-tunnel"), result); + } + + @Test + void tunnelBinaryFromCodeSourcePath_resolvesFromClassDir() { + // In dev/test, code source is a directory like build/classes/java/main + var classDir = Path.of("/project/lib/build/classes/java/main"); + var result = CallManager.tunnelBinaryFromCodeSourcePath(classDir); + // Goes up two levels from main -> classes, then looks for bin/signal-call-tunnel + assertEquals(Path.of("/project/lib/build/classes/bin/signal-call-tunnel"), result); + } + + @Test + void tunnelBinaryFromCodeSourcePath_deeplyNestedPath() { + var jarPath = Path.of("/home/user/.local/share/signal-cli/lib/signal-cli.jar"); + var result = CallManager.tunnelBinaryFromCodeSourcePath(jarPath); + assertEquals(Path.of("/home/user/.local/share/signal-cli/bin/signal-call-tunnel"), result); + } + // ======================================================================== // Helpers that reproduce the documented logic from handleStateChange and // endCall, allowing us to verify the state machine rules without needing