diff --git a/data/dbus-interfaces/meson.build b/data/dbus-interfaces/meson.build
index 4ba832661..c96bbbb4d 100644
--- a/data/dbus-interfaces/meson.build
+++ b/data/dbus-interfaces/meson.build
@@ -1,5 +1,6 @@
dbus_interfaces = [
'org.gnome.Shell.Extensions.xml',
+ 'org.gnome.Shell.Introspect.xml',
'org.gnome.Shell.PadOsd.xml',
'org.gnome.Shell.Screencast.xml',
'org.gnome.Shell.Screenshot.xml',
diff --git a/data/dbus-interfaces/org.gnome.Shell.Introspect.xml b/data/dbus-interfaces/org.gnome.Shell.Introspect.xml
new file mode 100644
index 000000000..10c48d635
--- /dev/null
+++ b/data/dbus-interfaces/org.gnome.Shell.Introspect.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/gnome-shell-dbus-interfaces.gresource.xml b/data/gnome-shell-dbus-interfaces.gresource.xml
index 5183672c9..ba148ca1f 100644
--- a/data/gnome-shell-dbus-interfaces.gresource.xml
+++ b/data/gnome-shell-dbus-interfaces.gresource.xml
@@ -40,6 +40,7 @@
org.gnome.SettingsDaemon.Wacom.xml
org.gnome.Shell.AudioDeviceSelection.xml
org.gnome.Shell.Extensions.xml
+ org.gnome.Shell.Introspect.xml
org.gnome.Shell.HotplugSniffer.xml
org.gnome.Shell.PerfHelper.xml
org.gnome.Shell.PortalHelper.xml
diff --git a/data/org.gnome.shell.gschema.xml.in b/data/org.gnome.shell.gschema.xml.in
index a7251cd06..87898a36e 100644
--- a/data/org.gnome.shell.gschema.xml.in
+++ b/data/org.gnome.shell.gschema.xml.in
@@ -90,6 +90,14 @@
adapter is ever seen not to have devices associated to it.
+
+ false
+ Enable introspection API
+
+ Enables a D-Bus API that allows to introspect the application state of
+ the shell.
+
+
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 75a6c3b12..a3dd2be08 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -16,6 +16,7 @@
misc/history.js
misc/ibusManager.js
misc/inputMethod.js
+ misc/introspect.js
misc/jsParse.js
misc/keyboardManager.js
misc/loginManager.js
diff --git a/js/misc/introspect.js b/js/misc/introspect.js
new file mode 100644
index 000000000..fa0a66e00
--- /dev/null
+++ b/js/misc/introspect.js
@@ -0,0 +1,111 @@
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Lang = imports.lang;
+const Meta = imports.gi.Meta;
+const Shell = imports.gi.Shell;
+
+const INTROSPECT_SCHEMA = 'org.gnome.shell';
+const INTROSPECT_KEY = 'introspect';
+const APP_WHITELIST = ['org.freedesktop.impl.portal.desktop.gtk'];
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const IntrospectDBusIface = loadInterfaceXML('org.gnome.Shell.Introspect');
+
+var IntrospectService = new Lang.Class({
+ Name: 'IntrospectService',
+
+ _init() {
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(IntrospectDBusIface,
+ this);
+ this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Introspect');
+ Gio.DBus.session.own_name('org.gnome.Shell.Introspect',
+ Gio.BusNameOwnerFlags.REPLACE,
+ null, null);
+
+ this._runningApplications = {};
+ this._runningApplicationsDirty = true;
+ this._activeApplication = null;
+ this._activeApplicationDirty = true;
+
+ this._appSystem = Shell.AppSystem.get_default();
+ this._appSystem.connect('app-state-changed',
+ () => {
+ this._runningApplicationsDirty = true;
+ this._syncRunningApplications();
+ });
+
+ this._settings = new Gio.Settings({ schema_id: INTROSPECT_SCHEMA });
+
+ let tracker = Shell.WindowTracker.get_default();
+ tracker.connect('notify::focus-app',
+ () => {
+ this._activeApplicationDirty = true;
+ this._syncRunningApplications();
+ });
+
+ this._syncRunningApplications();
+ },
+
+ _isStandaloneApp(app) {
+ let windows = app.get_windows();
+
+ return app.get_windows().some(w => w.transient_for == null);
+ },
+
+ _isIntrospectEnabled() {
+ return this._settings.get_boolean(INTROSPECT_KEY);
+ },
+
+ _isSenderWhitelisted(sender) {
+ return APP_WHITELIST.includes(sender);
+ },
+
+ _syncRunningApplications() {
+ let tracker = Shell.WindowTracker.get_default();
+ let apps = this._appSystem.get_running();
+ let seatName = "seat0";
+ let newRunningApplications = {};
+
+ let newActiveApplication = null;
+ let focusedApp = tracker.focus_app;
+
+ for (let app of apps) {
+ let appInfo = {};
+ let isAppActive = (focusedApp == app);
+
+ if (!this._isStandaloneApp(app))
+ continue;
+
+ if (isAppActive) {
+ appInfo['active-on-seats'] = new GLib.Variant('as', [seatName]);
+ newActiveApplication = app.get_id();
+ }
+
+ newRunningApplications[app.get_id()] = appInfo;
+ }
+
+ if (this._runningApplicationsDirty ||
+ (this._activeApplicationDirty &&
+ this._activeApplication != newActiveApplication)) {
+ this._runningApplications = newRunningApplications;
+ this._activeApplication = newActiveApplication;
+
+ this._dbusImpl.emit_signal('RunningApplicationsChanged', null);
+ }
+ this._runningApplicationsDirty = false;
+ this._activeApplicationDirty = false;
+ },
+
+ GetRunningApplicationsAsync(params, invocation) {
+ if (!this._isIntrospectEnabled() &&
+ !this._isSenderWhitelisted(invocation.get_sender())) {
+ invocation.return_error_literal(Gio.DBusError,
+ Gio.DBusError.ACCESS_DENIED,
+ 'App introspection not allowed');
+ return;
+ }
+
+ invocation.return_value(new GLib.Variant('(a{sa{sv}})', [this._runningApplications]));
+ }
+});
diff --git a/js/ui/main.js b/js/ui/main.js
index 9e7ce8706..f87b1ad02 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -20,6 +20,7 @@ const Environment = imports.ui.environment;
const ExtensionSystem = imports.ui.extensionSystem;
const ExtensionDownloader = imports.ui.extensionDownloader;
const InputMethod = imports.misc.inputMethod;
+const Introspect = imports.misc.introspect;
const Keyboard = imports.ui.keyboard;
const MessageTray = imports.ui.messageTray;
const ModalDialog = imports.ui.modalDialog;
@@ -82,6 +83,7 @@ var keyboard = null;
var layoutManager = null;
var kbdA11yDialog = null;
var inputMethod = null;
+var introspectService = null;
let _startDate;
let _defaultCssStylesheet = null;
let _cssStylesheet = null;
@@ -187,6 +189,8 @@ function _initializeUI() {
windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
componentManager = new Components.ComponentManager();
+ introspectService = new Introspect.IntrospectService();
+
layoutManager.init();
overview.init();