From 2df034c01bdf2e319e742a06061577f957d4af60 Mon Sep 17 00:00:00 2001 From: Shaheen Gandhi Date: Tue, 17 Mar 2026 16:02:50 -0700 Subject: [PATCH] Derive install dir from jar location instead of nonexistent property The signal.cli.install.dir system property was never set by the Gradle start script or anywhere else. Replace it with code source detection: resolve the jar's parent directory to find the install root, then look for bin/signal-call-tunnel relative to that. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/CALL_TUNNEL.md | 2 +- .../signal/manager/helper/CallManager.java | 28 ++++++++++++---- .../manager/helper/CallManagerTest.java | 33 +++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) 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