blob: 8bda62b217dc1dd908cb172f89f3350738003556 [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.
* - At time of writing (v 1.4), only handles incoming gdbus calls. But could be easily extended to
* handle outgoing gdbus calls.
*
* @category gdbus
*/
public class GDBus {
final static String SWT_GDBUS_VERSION_INFO = "SWT_LIB GDbus firing up. Implementation v1.5";
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 in the format of: (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.
* Note, input argument(s) are provided as an Object[] array. You need to cast items manually.
* Output must always be an Object[] array or null. (E.g Object[] with only 1 element in it).
*
* Here are examples that you can base your methods off of:
* // Multiple input types and multiple output types.
* // From command line, it can be called like this:
* // gdbus call --session --dest org.eclipse.swt --object-path /org/eclipse/swt --method org.eclipse.swt.typeTest true "world" 1234
* // The call will return a tuple (struct) like: (true, 'world', 5678)
* GDBusMethod typeTest = new GDBusMethod(
* "typeTest",
* new String [][] {{OS.DBUS_TYPE_BOOLEAN, "boolean Test Val"}, {OS.DBUS_TYPE_STRING, "string Test Val"}, {OS.DBUS_TYPE_INT32, "int Test Val"}},
* new String [][] {{OS.DBUS_TYPE_BOOLEAN, "boolean Response"}, {OS.DBUS_TYPE_STRING, "string Test Response"}, {OS.DBUS_TYPE_INT32, "int Test Response"}},
* (args) -> {
* System.out.println(args[0] + " " + args[1] + " " + args[2]); // A
* return new Object[] {Boolean.valueOf(true), "world", Integer.valueOf(5678)} ;
* });
*
* // Single input and single output. Observe input is an array with one item. Output is an array with one item.
* // It can be called from cmd via:
* // gdbus call --session --dest org.eclipse.swt --object-path /org/eclipse/swt --method org.eclipse.swt.singleValueTest 123
* // The return is a tuple with one element: (456,)
* GDBusMethod singleValTest = new GDBusMethod(
* "singleValueTest",
* new String[][] {{OS.DBUS_TYPE_INT32, "int val"}},
* new String[][] {{OS.DBUS_TYPE_INT32, "int ret"}},
* (arg) -> {
* System.out.println("Input int: " + arg[0]);
* return new Object[] {(Integer) 456};
* });
*
* // A simple method for clients to check if org.eclipse.swt is up and running. (getting/parsing interface xml is too tedious).
* // Reached via: gdbus call --session --dest org.eclipse.swt --object-path /org/eclipse/swt --method org.eclipse.swt.PingResponse
* // return is: (true,) #i.e (b)
* new GDBusMethod(
* "PingResponse",
* new String [0][0], // No input args.
* new String [][] {{OS.DBUS_TYPE_BOOLEAN, "Ping boolean response"}}, // Single boolean res
* (args) -> {
* return new Object[] {true}; // You should 'g_variant_unref(result)' on client side.
* })
*
*/
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;
}
}
/**
* Instantiate GDBus for use by SWT.
* Note, a new SWT instance that runs this "Steals" the session bus,
* but upon termination it returns the session back to the previous owner.
*
* To make this more flexible we append appName (derived from the
* application executable but can be set with the command-line argument
* -name) to the session name.
*
* @param methods GDBus methods that we should handle.
* @param appName appName to append to GDBus object name if not null
*/
public static void init (GDBusMethod[] methods, String appName) {
String serviceName = DBUS_SERVICE_NAME;
if (!initialized)
initialized = true;
else
return;
if (methods == null || methods.length == 0) {
System.err.println("SWT Error, no gdbus methods to initialize.");
return;
}
if (appName != null) {
// GDBus allows alphanumeric characters, underscores and hyphens.
// https://gitlab.gnome.org/GNOME/glib/blob/master/gio/gdbusutils.c
// Replace invalid GDBus characters with hyphens.
appName = appName.replaceAll("[^0-9A-Za-z_.\\-]", "-");
serviceName += "." + appName;
}
gdbusMethods = Arrays.asList(methods);
int owner_id = OS.g_bus_own_name(OS.G_BUS_TYPE_SESSION,
Converter.javaStringToCString(serviceName),
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: " + serviceName);
}
}
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$
onNameAcquired = new Callback (GDBus.class, "onNameAcquired", 3); //$NON-NLS-1$
onNameLost = new Callback (GDBus.class, "onNameLost", 3); //$NON-NLS-1$
handleMethod = new Callback (GDBus.class, "handleMethod", 8); //$NON-NLS-1$
String swt_lib_versions = OS.getEnvironmentalVariable (OS.SWT_LIB_VERSIONS); // Note, this is read in multiple places.
if (swt_lib_versions != null && swt_lib_versions.equals("1")) {
System.out.println(SWT_GDBUS_VERSION_INFO);
}
}
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 onBusAcquired (long gDBusConnection, long const_gchar_name, long user_data) {
long 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 [] error = new long [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 [] error = new long [1];
long interface_info = OS.g_dbus_node_info_lookup_interface(gdBusNodeInfo, Converter.javaStringToCString(INTERFACE_NAME));
long 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 onNameAcquired (long connection, long name, long 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 onNameLost (long connection, long name, long user_data) {
// Currently not used, but can be used if losing the gdbus name should trigger something.
// As a note, if another instance steals the name, upon it's terminate the session is returned to it's former owner.
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 handleMethod (
long connection, long sender,
long object_path, long interface_name,
long method_name, long gvar_parameters,
long invocation, long user_data) {
long 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);
if (returnVal == null || (returnVal instanceof Object [])) {
resultGVariant = convertJavaToGVariant(returnVal);
} else {
System.err.println("SWT GDBus error processing user return value: " + returnVal.toString() + ". Return value must be an Object[] or null.");
}
break;
}
}
} catch (Exception e) {
System.err.println("SWT GDBUS ERROR: Error in handling method: \n" + e.getMessage());
} finally {
// - GDBus method should always return to prevent caller from hanging.
// - Note, result must be a tuple. (or null (0)..).
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 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 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);
}
if (OS.g_variant_is_of_type(gVariant, OS.G_VARIANT_TYPE_BOOLEAN)) {
return Boolean.valueOf(OS.g_variant_get_boolean(gVariant));
}
if (OS.g_variant_is_of_type(gVariant, OS.G_VARIANT_TYPE_IN32)) {
return Integer.valueOf(OS.g_variant_get_int32(gVariant));
}
// <You can add more primitive types here>
//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);
System.err.println("SWT GDBus: Error. Unhandled variant type (i.e DBus type): " + typeString +
" You probably need to update (SWT) GDBus.java:convertGVariantToJavaHelper() to support this type.");
SWT.error (SWT.ERROR_INVALID_ARGUMENT);
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 convertJavaToGVariant(Object javaObject) throws SWTException {
if (javaObject == null) {
return 0;
}
if (javaObject instanceof String) {
return OS.g_variant_new_string (Converter.javaStringToCString((String) javaObject));
}
if (javaObject instanceof Boolean) {
return OS.g_variant_new_boolean((Boolean) javaObject);
}
if (javaObject instanceof Integer) {
return OS.g_variant_new_int32((Integer) javaObject);
}
// <You can add more primitive 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 variants[] = new long [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() + " You probably need to update (SWT) GDBus.java:convertJavaToGVariant()");
throw new SWTException(SWT.ERROR_INVALID_ARGUMENT);
}
}