blob: 21bbf2f99ccc59da19c32a4e80b4041868b29547 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018 Red Hat and others. All rights reserved.
* The contents of this file are made available under the terms
* of the GNU Lesser General Public License (LGPL) Version 2.1 that
* accompanies this distribution (lgpl-v21.txt). The LGPL is also
* available at http://www.gnu.org/licenses/lgpl.html. If the version
* of the LGPL at http://www.gnu.org is different to the version of
* the LGPL accompanying this distribution and there is any conflict
* between the two license versions, the terms of the LGPL accompanying
* this distribution shall govern.
*
* Contributors:
* Red Hat - initial API and implementation
*******************************************************************************/
package org.eclipse.swt.internal;
import java.util.*;
import java.util.function.*;
import org.eclipse.swt.*;
import org.eclipse.swt.internal.gtk.*;
/**
* General purpose DBus interface for SWT to interact with the operating system and vice versa.
* (See also WebkitGDBus for the webkit specific gdbus interface).
*
* This implementation uses GDBus (Gnome DBus) to implement the DBus interface.
*
* It can be reached via:
* gdbus call --session --dest org.eclipse.swt --object-path /org/eclipse/swt --method org.eclipse.swt.YOUR_METHOD YOUR_ARGS
* where YOUR_ARGS can be something like "MyString" or "['/tmp/myFile', '/tmp/myFile2']" etc..
* For hygiene purposes, GVariant/GDBus native types/values should *never* leave this class. Convert on the way in/out.
*
* Technical notes:
* - Concurrent gdbus names can co-exist. (i.e, multiple session names in single proc).
* Meaning if you don't like org.eclipse.swt, you can add more session names.
* - This implementation is only a small subset of GDBus.
* E.g not all types are translated and not functionality implemented. Add them as you need them.
*
* @category gdbus
*/
public class GDBus {
public static class GDBusMethod {
final private String name;
final private Function<Object[], Object[]> userFunction;
final private String methodArgsXmlSignature;
/**
* Create a method that GDBus will listen to.
*
* @param name of the method. It will be part of 'org.eclipse.swt.NAME'
* @param inputArgs 2D array pair of Strings : (DBUS_TYPE_*, argument_name). Where argument_name is only so that it's seen by command line by user.
* @param outputArgs Same as inputArgs, but for returning values.
* @param userFunction A Function<Object[],Object[]>, that you would like to run when the user calls the method over gdbus.
*/
public GDBusMethod(String name, String [][] inputArgs, String [][] outputArgs, Function<Object[], Object[]> userFunction) {
this.name = name;
this.userFunction = userFunction;
StringBuilder gdbBusArgsXml = new StringBuilder();
for (String [] dataType : inputArgs) {
gdbBusArgsXml.append(" <arg type='" + dataType[0] + "' name='" + dataType[1] + "' direction='in'/>\n");
}
for (String [] dataType : outputArgs) { // I haven't tested output args, but should work if types get converted properly.
gdbBusArgsXml.append(" <arg type='" + dataType[0] + "' name='" + dataType[1] + "' direction='out'/>\n");
}
methodArgsXmlSignature = gdbBusArgsXml.toString();
}
private String getName() {
return name;
}
private Function<Object[], Object[]> getUserFunction() {
return userFunction;
}
private String getMethodArgsXmlSignature() {
return methodArgsXmlSignature;
}
}
public static void init (GDBusMethod[] methods) {
if (!initialized)
initialized = true;
else
return;
if (methods == null || methods.length == 0) {
System.err.println("SWT Error, no gdbus methods to initialize.");
return;
}
gdbusMethods = Arrays.asList(methods);
int owner_id = OS.g_bus_own_name(OS.G_BUS_TYPE_SESSION,
Converter.javaStringToCString(DBUS_SERVICE_NAME),
OS.G_BUS_NAME_OWNER_FLAGS_REPLACE | OS.G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT, // Allow name to be used by other eclipse instances.
onBusAcquired.getAddress(),
onNameAcquired.getAddress(), // name_acquired_handler
onNameLost.getAddress(), // name_lost_handler
0, // user_data
0); // user_data_free_func
if (owner_id == 0) {
System.err.println("SWT GDBus: Failed to aquire bus name: " + DBUS_SERVICE_NAME);
}
}
private static boolean initialized;
private static List<GDBusMethod> gdbusMethods;
private static final String DBUS_SERVICE_NAME = "org.eclipse.swt";
private static final String DBUS_OBJECT_NAME = "/org/eclipse/swt";
private static final String INTERFACE_NAME = "org.eclipse.swt";
private static Callback onBusAcquired;
private static Callback onNameAcquired;
private static Callback onNameLost;
private static Callback handleMethod;
static {
onBusAcquired = new Callback (GDBus.class, "onBusAcquired", 3); //$NON-NLS-1$
if (onBusAcquired.getAddress () == 0) SWT.error (SWT.ERROR_NO_MORE_CALLBACKS);
onNameAcquired = new Callback (GDBus.class, "onNameAcquired", 3); //$NON-NLS-1$
if (onNameAcquired.getAddress () == 0) SWT.error (SWT.ERROR_NO_MORE_CALLBACKS);
onNameLost = new Callback (GDBus.class, "onNameLost", 3); //$NON-NLS-1$
if (onNameLost.getAddress () == 0) SWT.error (SWT.ERROR_NO_MORE_CALLBACKS);
handleMethod = new Callback (GDBus.class, "handleMethod", 8); //$NON-NLS-1$
if (handleMethod.getAddress () == 0) SWT.error (SWT.ERROR_NO_MORE_CALLBACKS);
String swt_lib_versions = OS.getEnvironmentalVariable (OS.SWT_LIB_VERSIONS); // Note, this is read in multiple places. //leotask move string to a constant in OS.java
if (swt_lib_versions != null && swt_lib_versions.equals("1")) {
System.out.println("SWT_LIB GDbus implementation v1.");
}
}
static void teardown_gdbus() {
// Currently GDBus is persistent.
// If ever needed, gdbus can be disposed via:
// g_bus_unown_name (owner_id); // owner_id would need to be made global
// g_dbus_node_info_unref (gdBusNodeInfo); // introspection_data Would need to be made global
}
/**
* @return void. (No return value actually returned to OS. Just SWT mechanism dicates 'long' is returned.
*/
@SuppressWarnings("unused") // Callback only called directly by JNI.
private static long /*int*/ onBusAcquired (long /*int*/ gDBusConnection, long /*int*/ const_gchar_name, long /*int*/ user_data) {
long /*int*/ gdBusNodeInfo;
{ // Generate and parse DBus XML interface.
StringBuilder dbus_introspection_xml = new StringBuilder();
dbus_introspection_xml.append("<node><interface name='" + INTERFACE_NAME + "'>\n");
for (GDBusMethod method : gdbusMethods) {
dbus_introspection_xml.append(" <method name='" + method.name + "'>\n");
dbus_introspection_xml.append(" " + method.getMethodArgsXmlSignature() + "\n");
dbus_introspection_xml.append(" </method>\n");
}
dbus_introspection_xml.append("</interface></node>");
long /*int*/ [] error = new long /*int*/ [1];
gdBusNodeInfo = OS.g_dbus_node_info_new_for_xml(Converter.javaStringToCString(dbus_introspection_xml.toString()), error);
if (gdBusNodeInfo == 0 || error[0] != 0) {
System.err.println("SWT GDBus: Failed to get introspection data");
}
assert gdBusNodeInfo != 0 : "SWT GDBus: introspection data should not be 0";
}
{ // Register object
long /*int*/ [] error = new long /*int*/ [1];
long /*int*/ interface_info = OS.g_dbus_node_info_lookup_interface(gdBusNodeInfo, Converter.javaStringToCString(INTERFACE_NAME));
long /*int*/ vtable [] = { handleMethod.getAddress(), 0, 0 };
OS.g_dbus_connection_register_object(
gDBusConnection,
Converter.javaStringToCString(DBUS_OBJECT_NAME),
interface_info,
vtable,
0, // user_data
0, // user_data_free_func
error);
if (error[0] != 0) {
System.err.println("SWT GDBus: Failed to register object: " + DBUS_OBJECT_NAME);
return 0;
}
}
// Developer note:
// To verify that a gdbus interface is regisetered on the gdbus, you can use the 'gdbus' utility.
// e.g:
// gdbus introspect --session --dest org.eclipse <Press TAB KEY> // it should expand to something like: (uniqueID might be appended at the end).
// gdbus introspect --session --dest org.eclipse.swt // you can then get object info like:
// gdbus introspect --session --dest org.eclipse.swt --object-path /org/eclipse/swt ... etc
return 0; // Actual callback is void.
}
@SuppressWarnings("unused") // Callback Only called directly by JNI.
private static long /*int*/ onNameAcquired (long /*int*/ connection, long /*int*/ name, long /*int*/ user_data) {
// Currently not used, but can be used if acquring the gdbus name should trigger something to load.
return 0;
}
@SuppressWarnings("unused") // Callback Only called directly by JNI.
private static long /*int*/ onNameLost (long /*int*/ connection, long /*int*/ name, long /*int*/ user_data) {
System.err.println("SWT GDBus.java: Lost GDBus name. Maybe stolen?");
return 0;
}
/**
* This is called when a client calls one of the GDBus methods.
*
* Developer note:
* This method can be reached directly from GDBus cmd utility:
* gdbus call --session --dest org.eclipse.swt --object-path /org/eclipse/swt --method org.eclipse.swt.<your method>
* Tip: Use tab-completion.
*
* @param connection GDBusConnection
* @param sender const gchar
* @param object_path const gchar
* @param interface_name const gchar
* @param method_name const gchar
* @param gvar_parameters GVariant
* @param invocation GDBusMethodInvocation
* @param user_data gpointer
* @return
*/
@SuppressWarnings("unused") // Callback only called directly by JNI.
private static long /*int*/ handleMethod (
long /*int*/ connection, long /*int*/ sender,
long /*int*/ object_path, long /*int*/ interface_name,
long /*int*/ method_name, long /*int*/ gvar_parameters,
long /*int*/ invocation, long /*int*/ user_data) {
long /*int*/ resultGVariant = 0;
try {
String java_method_name = Converter.cCharPtrToJavaString(method_name, false);
for (GDBusMethod gdbusMethod : gdbusMethods) {
if (gdbusMethod.getName().equals(java_method_name)) {
Object [] args = convertGVariantToJava(gvar_parameters);
Object [] returnVal = gdbusMethod.getUserFunction().apply(args); // Return value currently not used. Can be added/implemented if required.
resultGVariant = convertJavaToGVariant(returnVal);
break;
}
}
} catch (Exception e) {
System.err.println("SWT GDBUS ERROR: Error in handling method.");
} finally {
// - GDBus method should always return to prevent caller from hanging.
// - Note, result must be a tuple. (or null..).
OS.g_dbus_method_invocation_return_value(invocation, resultGVariant);
}
return 0; // void return value.
}
/**
* Converts the given GVariant(s) to Java Object(s).
* If GVariant is not an array, this returns an Object[] array with one element.
*
* Only subset of types is currently supported, add type(s) as/when you need them.
* For inspiration, see WebkitGDBus.java:convert..()
*
* @param gVariant a pointer to the native GVariant
*/
private static Object[] convertGVariantToJava(long /*int*/ gVariant) {
Object retVal = convertGVariantToJavaHelper(gVariant);
if (retVal instanceof Object[]) {
return (Object[]) retVal;
} else {
System.err.println("SWT GDBus Error converting arguments : Expecting object array, received Object.");
return null;
}
}
private static Object convertGVariantToJavaHelper(long /*int*/ gVariant){
// - Developer note:
// When instantiating GDBus dynamically (as we do),
// GDBus's 'Parameters' is _always_ a tuple of stuff.
// E.g If you pass it only a single argument, then you will have a tuple with one object (s).
// As such, '1) convert specific types' is only reached, at the earliest
// on the 2nd invocation of this recursive function.
// - Note, tuples '()' are not the same as arrays 'a'. They're treated in different ways.
// 1) Convert specific types. (non-array).
if (OS.g_variant_is_of_type(gVariant, OS.G_VARIANT_TYPE_STRING)){
return Converter.cCharPtrToJavaString(OS.g_variant_get_string(gVariant, null), false);
}
// <add your primitive types>
//2) Handle struct of defined types. (Sort of like arrays, but note, tuples!=array).
if (OS.g_variant_is_of_type(gVariant, OS.G_VARIANT_TYPE_TUPLE)) {
int length = (int)OS.g_variant_n_children (gVariant);
Object[] result = new Object[length];
for (int i = 0; i < length; i++) {
result[i] = convertGVariantToJavaHelper (OS.g_variant_get_child_value(gVariant, i));
}
return result;
}
// 2 b) Handle an array of Strings. Same as above, except type is String.
if (OS.g_variant_is_of_type(gVariant, OS.G_VARIANT_TYPE_STRING_ARRAY)) {
int length = (int)OS.g_variant_n_children (gVariant);
String[] result = new String[length];
for (int i = 0; i < length; i++) {
result[i] = (String) convertGVariantToJavaHelper (OS.g_variant_get_child_value(gVariant, i));
}
return result;
}
String typeString = Converter.cCharPtrToJavaString(OS.g_variant_get_type_string(gVariant), false);
SWT.error (SWT.ERROR_INVALID_ARGUMENT, new Throwable("SWT GDBus: Unhandled variant type " + typeString ));
return null;
}
/**
* Converts the given Java Object to a GVariant * representation.
* (Only subset of types is currently supported).
*
* We assume that input Object may contain invalid types.
*
* @return pointer GVariant *
*/
private static long /*int*/ convertJavaToGVariant(Object javaObject) throws SWTException {
if (javaObject == null) {
return 0;
}
if (javaObject instanceof String) {
return OS.g_variant_new_string (Converter.javaStringToCString((String) javaObject));
}
// Add your types here.
// Dangers:
// Null values and empty arrays can cause problems.
// - DBus doesn't have notion of 'null'.
// - DBus doesn't support empty arrays.
// If needed, see workaround implemented in WebkitGDBus.java
if (javaObject instanceof Object[]) {
Object[] arrayValue = (Object[]) javaObject;
int length = arrayValue.length;
long /*int*/ variants[] = new long /*int*/[length];
for (int i = 0; i < length; i++) {
variants[i] = convertJavaToGVariant(arrayValue[i]);
}
return OS.g_variant_new_tuple(variants, length);
}
System.err.println("SWT GDbus: Invalid object being returned to caller: " + javaObject.toString());
throw new SWTException(SWT.ERROR_INVALID_ARGUMENT, "Given object is not valid: " + javaObject.toString());
}
}