Skip to content

Commit

Permalink
Refactor the Evince DBus connection to use the java-dbus bindings ins…
Browse files Browse the repository at this point in the history
…tead of spawning commands
  • Loading branch information
taaem committed Jun 30, 2024
1 parent c67a0d6 commit 970bce6
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 61 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ dependencies {
// implementation(files("lib/JavaDDEx64.dll"))

// D-Bus Java bindings
implementation("com.github.hypfvieh:dbus-java:3.3.2")
implementation("com.github.hypfvieh:dbus-java-core:5.0.0")
implementation("org.slf4j:slf4j-simple:2.0.13")

// Unzipping tar.xz/tar.bz2 files on Windows containing dtx files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import com.intellij.notification.NotificationType
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.run.linuxpdfviewer.ViewerConversation
import nl.hannahsten.texifyidea.util.runCommand
import org.freedesktop.dbus.connections.impl.DBusConnection
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder
import org.freedesktop.dbus.errors.NoReply
import org.freedesktop.dbus.errors.ServiceUnknown
import org.freedesktop.dbus.exceptions.DBusException
import org.freedesktop.dbus.types.UInt32
import org.gnome.evince.Daemon
import org.gnome.evince.SyncViewSourcePointStruct
import org.gnome.evince.Window

/**
* Send commands to Evince.
Expand All @@ -31,6 +34,13 @@ object EvinceConversation : ViewerConversation() {
*/
private const val EVINCE_DAEMON_NAME = "org.gnome.evince.Daemon"

/**
* Object path of the Evince daemon. Together with the object name, this allows us to find the
* D-Bus object which allows us to execute the FindDocument function, which is exported on the D-Bus
* by Evince.
*/
private const val EVINCE_WINDOW_PATH = "/org/gnome/evince/Window/0"

/**
* This variable will hold the latest known Evince process owner. We need to know the owner of the pdf file in order to execute forward search.
*/
Expand Down Expand Up @@ -63,10 +73,25 @@ object EvinceConversation : ViewerConversation() {
}

if (processOwner != null) {
// Theoretically we should use the Java D-Bus bindings as well to call SyncView, but that did
// not succeed with a NoReply exception, so we will execute a command via the shell
val command = "gdbus call --session --dest $processOwner --object-path /org/gnome/evince/Window/0 --method org.gnome.evince.Window.SyncView $sourceFilePath '($line, 1)' 0"
runCommand("bash", "-c", command)
try {
// Get DBusConnection
DBusConnectionBuilder.forSessionBus().build().use { connection ->
// Get the Object corresponding to the interface and call the functions to the processOwner
val window = connection.getRemoteObject(processOwner, EVINCE_WINDOW_PATH, Window::class.java)

// Sync the Evince view to the current position
try {
window.SyncView(sourceFilePath, SyncViewSourcePointStruct(line, -1), UInt32(0))
}
catch (ignored: NoReply) {}
catch (e: ServiceUnknown) {
Notification("LaTeX", "Cannot sync position to Evince", "Please update Evince and then try again.", NotificationType.ERROR).notify(project)
}
}
}
catch (e: DBusException) {
Notification("LaTeX", "Cannot sync position to Evince", "The Connection could not be established.", NotificationType.ERROR).notify(project)
}
}
else {
// If the user used the forward search menu action
Expand All @@ -87,20 +112,25 @@ object EvinceConversation : ViewerConversation() {
* @param pdfFilePath Full path to the pdf file to find the owner of.
*/
private fun findProcessOwner(pdfFilePath: String, project: Project) {
// Initialize a session bus
val connection = DBusConnection.getConnection(DBusConnection.DBusBusType.SESSION)

// Get the Daemon object using its bus name and object path
val daemon = connection.getRemoteObject(EVINCE_DAEMON_NAME, EVINCE_DAEMON_PATH, Daemon::class.java)

// Call the method on the D-Bus by using the function we defined in the Daemon interface
// Catch a NoReply, because it is unknown why Evince cannot start so we don't try to fix that
try {
processOwner = daemon.FindDocument("file://$pdfFilePath", true)
// Get DBusConnection
DBusConnectionBuilder.forSessionBus().build().use { connection ->
// Get the Daemon object using its bus name and object path
val daemon = connection.getRemoteObject(EVINCE_DAEMON_NAME, EVINCE_DAEMON_PATH, Daemon::class.java)

// Call the method on the D-Bus by using the function we defined in the Daemon interface
// Catch a NoReply, because it is unknown why Evince cannot start so we don't try to fix that
try {
processOwner = daemon.FindDocument("file://$pdfFilePath", true)
}
catch (ignored: NoReply) {}
catch (e: ServiceUnknown) {
Notification("LaTeX", "Cannot communicate to Evince", "Please update Evince and then try again.", NotificationType.ERROR).notify(project)
}
}
}
catch (ignored: NoReply) {}
catch (e: ServiceUnknown) {
Notification("LaTeX", "Cannot communicate to Evince", "Please update Evince and then try again.", NotificationType.ERROR).notify(project)
catch (e: DBusException) {
Notification("LaTeX", "Cannot communicate to Evince", "The connection could not be established.", NotificationType.ERROR).notify(project)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import com.intellij.openapi.application.PathManager
import com.intellij.openapi.project.Project
import kotlinx.coroutines.*
import nl.hannahsten.texifyidea.util.SystemEnvironment
import java.io.BufferedReader
import org.freedesktop.dbus.connections.impl.DBusConnection
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder
import org.gnome.evince.Window
import java.io.IOException
import java.io.InputStreamReader

/**
* Listen on the D-Bus for inverse search signals coming from Evince.
Expand All @@ -22,6 +23,10 @@ object EvinceInverseSearchListener {

private var currentCoroutineScope = CoroutineScope(Dispatchers.Default)

private val sessionConnection: DBusConnection = DBusConnectionBuilder.forSessionBus().build()

private var syncSourceHandler: AutoCloseable? = null

/**
* Starts a listener which listens for inverse search actions from Evince.
*/
Expand All @@ -33,6 +38,11 @@ object EvinceInverseSearchListener {
return
}

// Check if we already have a listener
if (syncSourceHandler != null) {
return
}

// Run in a coroutine so the main thread can continue
// If the program finishes, the listener will stop as well
currentCoroutineScope.launch {
Expand All @@ -44,44 +54,9 @@ object EvinceInverseSearchListener {
* Start listening for backward search calls on the D-Bus.
*/
private fun startListening() {
try {
// Listen on the session D-Bus to catch SyncSource signals emitted by Evince
val commands = arrayOf("dbus-monitor", "--session")
val process = Runtime.getRuntime().exec(commands)
val bufferedReader = BufferedReader(InputStreamReader(process.inputStream))
var line: String? = bufferedReader.readLine()

while (line != null && currentCoroutineScope.isActive) {
// Check if a SyncSource signal appeared from Evince and if so, read the contents
if (line.contains("interface=org.gnome.evince.Window; member=SyncSource")) {
// Get the value between quotes
val filenameLine = bufferedReader.readLine()
var filename = filenameLine.substring(filenameLine.indexOf("\"") + 1, filenameLine.lastIndexOf("\""))
filename = filename.replaceFirst("file://".toRegex(), "")

// Pass over the "struct {" line
bufferedReader.readLine()

// Get the location represented by the struct
val lineNumberLine = bufferedReader.readLine()
val lineNumberString = lineNumberLine.substring(lineNumberLine.indexOf("int32") + 6, lineNumberLine.length).trim()
val lineNumber = Integer.parseInt(lineNumberString)

// Sync the IDE
syncSource(filename, lineNumber)
}

// Check whether we would block before doing a blocking readLine call
// This is to ensure we can quickly stop this coroutine on plugin unload
while (!bufferedReader.ready()) {
Thread.sleep(100)
if (!currentCoroutineScope.isActive) return
}
line = bufferedReader.readLine()
}
}
catch (e: IOException) {
e.printStackTrace()
syncSourceHandler = sessionConnection.addSigHandler(Window.SyncSource::class.java) { signal ->
val filename = signal.sourceFile.replaceFirst("file://".toRegex(), "")
syncSource(filename, signal.sourcePoint.line)
}
}

Expand All @@ -95,7 +70,7 @@ object EvinceInverseSearchListener {
val path = PathManager.getBinPath()
val name = ApplicationNamesInfo.getInstance().scriptName

val command = "$path/$name.sh --line $lineNumber \"$filePath\""
val command = arrayOf("$path/$name.sh", "--line", lineNumber.toString(), "\"$filePath\"")

try {
Runtime.getRuntime().exec(command)
Expand All @@ -106,6 +81,10 @@ object EvinceInverseSearchListener {
}

fun unload() {
// Remove the listener
syncSourceHandler?.close()
// Properly close the connection
sessionConnection.close()
currentCoroutineScope.cancel(CancellationException(("Unloading the plugin")))
}
}
2 changes: 1 addition & 1 deletion src/org/gnome/evince/Daemon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ interface Daemon : DBusInterface {
* @param spawn Whether to spawn Evince or not.
* @return The name owner of the evince process for the given document URI.
*/
fun FindDocument(uri: String, spawn: Boolean?): String
fun FindDocument(uri: String?, spawn: Boolean): String?
}
14 changes: 14 additions & 0 deletions src/org/gnome/evince/SyncViewSourcePointStruct.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.gnome.evince

import org.freedesktop.dbus.Struct
import org.freedesktop.dbus.annotations.Position

/**
* Nested Object in DBus communication with evince, this specifies the position in the document
*
* @author Tim Klocke
*/
class SyncViewSourcePointStruct(
@field:Position(0) val line: Int,
@field:Position(1) val column: Int
) : Struct()
42 changes: 42 additions & 0 deletions src/org/gnome/evince/Window.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.gnome.evince

import org.freedesktop.dbus.annotations.DBusInterfaceName
import org.freedesktop.dbus.interfaces.DBusInterface
import org.freedesktop.dbus.messages.DBusSignal
import org.freedesktop.dbus.types.UInt32

/**
* Interface to communicate with an Evince Window over DBus, such as syncing in both directions
*
* @author Tim Klocke
*/
@Suppress("FunctionName")
@DBusInterfaceName("org.gnome.evince.Window")
interface Window : DBusInterface {

/**
* Sync the position of Evince to the corresponding position.
*
* @param sourceFile Path to the tex file, prepended with file://
* @param sourcePoint Position in the document to jump to.
* @param timestamp Timestamp
* @return
*/
fun SyncView(sourceFile: String?, sourcePoint: SyncViewSourcePointStruct?, timestamp: UInt32?)

/**
* Signal to sync the position of the editor to the specified position.
*
* @param path Path to the DBusObject
* @param sourceFile Path to the tex file, prepended with file://
* @param sourcePoint Position in the document to jump to.
* @param timestamp Timestamp
* @return
*/
class SyncSource(
path: String?,
val sourceFile: String,
val sourcePoint: SyncViewSourcePointStruct,
timestamp: UInt32
) : DBusSignal(path, sourceFile, sourcePoint, timestamp)
}

0 comments on commit 970bce6

Please sign in to comment.