[gjs: 1/2] Gio: Add non-crashing overrides for Gio.Settings



commit e3e2bc0c2b6c2f9e6c541e6acb7c19a17f3835d7
Author: Philip Chimento <philip chimento gmail com>
Date:   Sat Mar 9 21:10:04 2019 -0800

    Gio: Add non-crashing overrides for Gio.Settings
    
    Gio.Settings aborts intentionally if it is asked to load a nonexistent
    schema or access a nonexistent key. This is all well and good for C
    programs that install their own schemas, but a problem that many GNOME
    Shell extensions authors end up running into when trying to deal with
    multiple GNOME versions at once.
    
    Based on code from Cinnamon written by Michael Webster. See
    https://github.com/linuxmint/Cinnamon/blob/master/js/ui/overrides.js#L21-L98
    Used with permission of the copyright holder, see comments in #205.
    
    Closes: #205

 Makefile-test.am                                 |  10 ++
 configure.ac                                     |   1 +
 installed-tests/js/org.gnome.GjsTest.gschema.xml |  21 ++++
 installed-tests/js/testGio.js                    | 120 ++++++++++++++++++++++-
 modules/overrides/Gio.js                         |  94 ++++++++++++++++++
 5 files changed, 242 insertions(+), 4 deletions(-)
---
diff --git a/Makefile-test.am b/Makefile-test.am
index 0fe27eb3..acdcc677 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -56,6 +56,10 @@ CLEANFILES +=                                                \
        jsunit-resources.h                              \
        $(NULL)
 
+gsettings_SCHEMAS = installed-tests/js/org.gnome.GjsTest.gschema.xml
+EXTRA_DIST += $(gsettings_SCHEMAS)
+@GSETTINGS_RULES@
+
 ### TEST PROGRAMS ######################################################
 
 # gjs-tests.gtester checks private APIs and is run only uninstalled,
@@ -278,6 +282,11 @@ else
 COVERAGE_TESTS_ENVIRONMENT =
 endif
 
+installed-tests/js/testGio.log: gschemas.compiled
+
+gschemas.compiled: installed-tests/js/org.gnome.GjsTest.gschema.xml
+       $(AM_V_GEN)$(GLIB_COMPILE_SCHEMAS) --targetdir=. $(<D)
+
 # GJS_PATH is empty here since we want to force the use of our own
 # resources. G_FILENAME_ENCODING ensures filenames are not UTF-8.
 AM_TESTS_ENVIRONMENT =                                 \
@@ -290,6 +299,7 @@ AM_TESTS_ENVIRONMENT =                                      \
        export LSAN_OPTIONS="suppressions=$(abs_top_srcdir)/installed-tests/extra/lsan.supp"; \
        export NO_AT_BRIDGE=1;                          \
        export LC_ALL=$(TESTS_LOCALE);                  \
+       export GSETTINGS_SCHEMA_DIR="$(builddir)";      \
        $(COVERAGE_TESTS_ENVIRONMENT)                   \
        $(GTK_TESTS_ENVIRONMENT)                        \
        $(XVFB_START)                                   \
diff --git a/configure.ac b/configure.ac
index 3c44adba..cf543ccf 100644
--- a/configure.ac
+++ b/configure.ac
@@ -44,6 +44,7 @@ AC_PROG_MKDIR_P
 AC_PROG_LN_S
 AC_PROG_SED
 AC_PROG_AWK
+GLIB_GSETTINGS
 
 AX_COMPILER_FLAGS
 
diff --git a/installed-tests/js/org.gnome.GjsTest.gschema.xml 
b/installed-tests/js/org.gnome.GjsTest.gschema.xml
new file mode 100644
index 00000000..4d5df08f
--- /dev/null
+++ b/installed-tests/js/org.gnome.GjsTest.gschema.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+  <schema id="org.gnome.GjsTest" path="/org/gnome/GjsTest/">
+    <key name="window-size" type="(ii)">
+      <default>(-1, -1)</default>
+    </key>
+    <key name="maximized" type="b">
+      <default>false</default>
+    </key>
+    <key name="fullscreen" type="b">
+      <default>false</default>
+    </key>
+    <child name="sub" schema="org.gnome.GjsTest.Sub"/>
+  </schema>
+  <schema id="org.gnome.GjsTest.Sub">
+    <key name="marine" type="u">
+      <range min="1" max="100"/>
+      <default>10</default>
+    </key>
+  </schema>
+</schemalist>
diff --git a/installed-tests/js/testGio.js b/installed-tests/js/testGio.js
index 7feeb79c..b52358c1 100644
--- a/installed-tests/js/testGio.js
+++ b/installed-tests/js/testGio.js
@@ -1,7 +1,11 @@
-const Gio = imports.gi.Gio;
-const GObject = imports.gi.GObject;
+const {GLib, Gio, GObject} = imports.gi;
 
-const Foo = GObject.registerClass(class Foo extends GObject.Object {
+const Foo = GObject.registerClass({
+    Properties: {
+        boolval: GObject.ParamSpec.boolean('boolval', '', '',
+            GObject.ParamFlags.READWRITE, false),
+    }
+}, class Foo extends GObject.Object {
     _init(value) {
         super._init();
         this.value = value;
@@ -24,4 +28,112 @@ describe('ListStore iterator', function () {
             expect(f.value).toBe(i++);
         }
     });
-});
\ No newline at end of file
+});
+
+describe('Gio.Settings overrides', function () {
+    it("doesn't crash when forgetting to specify a schema ID", function () {
+        expect(() => new Gio.Settings()).toThrowError(/schema/);
+    });
+
+    it("doesn't crash when specifying a schema ID that isn't installed", function () {
+        expect(() => new Gio.Settings({schema: 'com.example.ThisDoesntExist'}))
+            .toThrowError(/schema/);
+    });
+
+    describe('with existing schema', function () {
+        const KINDS = ['boolean', 'double', 'enum', 'flags', 'int', 'int64',
+            'string', 'strv', 'uint', 'uint64', 'value'];
+        let settings;
+
+        beforeEach(function () {
+            settings = new Gio.Settings({schema: 'org.gnome.GjsTest'});
+        });
+
+        it("doesn't crash when resetting a nonexistent key", function () {
+            expect(() => settings.reset('foobar')).toThrowError(/key/);
+        });
+
+        it("doesn't crash when checking a nonexistent key", function () {
+            KINDS.forEach(kind => {
+                expect(() => settings[`get_${kind}`]('foobar')).toThrowError(/key/);
+            });
+        });
+
+        it("doesn't crash when setting a nonexistent key", function () {
+            KINDS.forEach(kind => {
+                expect(() => settings[`set_${kind}`]('foobar', null)).toThrowError(/key/);
+            });
+        });
+
+        it("doesn't crash when checking writable for a nonexistent key", function () {
+            expect(() => settings.is_writable('foobar')).toThrowError(/key/);
+        });
+
+        it("doesn't crash when getting the user value for a nonexistent key", function () {
+            expect(() => settings.get_user_value('foobar')).toThrowError(/key/);
+        });
+
+        it("doesn't crash when getting the default value for a nonexistent key", function () {
+            expect(() => settings.get_default_value('foobar')).toThrowError(/key/);
+        });
+
+        it("doesn't crash when binding a nonexistent key", function () {
+            const foo = new Foo();
+            expect(() => settings.bind('foobar', foo, 'boolval', Gio.SettingsBindFlags.GET))
+                .toThrowError(/key/);
+            expect(() => settings.bind_writable('foobar', foo, 'boolval', false))
+                .toThrowError(/key/);
+        });
+
+        it("doesn't crash when creating actions for a nonexistent key", function () {
+            expect(() => settings.create_action('foobar')).toThrowError(/key/);
+        });
+
+        it("doesn't crash when checking info about a nonexistent key", function () {
+            expect(() => settings.settings_schema.get_key('foobar')).toThrowError(/key/);
+        });
+
+        it("doesn't crash when getting a nonexistent sub-schema", function () {
+            expect(() => settings.get_child('foobar')).toThrowError(/foobar/);
+        });
+
+        it('still works with correct keys', function () {
+            const KEYS = ['window-size', 'maximized', 'fullscreen'];
+
+            KEYS.forEach(key => expect(settings.is_writable(key)).toBeTruthy());
+
+            expect(() => {
+                settings.set_value('window-size', new GLib.Variant('(ii)', [100, 100]));
+                settings.set_boolean('maximized', true);
+                settings.set_boolean('fullscreen', true);
+            }).not.toThrow();
+
+            expect(settings.get_value('window-size').deep_unpack()).toEqual([100, 100]);
+            expect(settings.get_boolean('maximized')).toEqual(true);
+            expect(settings.get_boolean('fullscreen')).toEqual(true);
+
+            expect(() => {
+                KEYS.forEach(key => settings.reset(key));
+            }).not.toThrow();
+
+            KEYS.forEach(key => expect(settings.get_user_value(key)).toBeNull());
+            expect(settings.get_default_value('window-size').deep_unpack()).toEqual([-1, -1]);
+            expect(settings.get_default_value('maximized').deep_unpack()).toEqual(false);
+            expect(settings.get_default_value('fullscreen').deep_unpack()).toEqual(false);
+
+            const foo = new Foo({boolval: true});
+            settings.bind('maximized', foo, 'boolval', Gio.SettingsBindFlags.GET);
+            expect(foo.boolval).toBeFalsy();
+            Gio.Settings.unbind(foo, 'boolval');
+            settings.bind_writable('maximized', foo, 'boolval', false);
+            expect(foo.boolval).toBeTruthy();
+
+            expect(settings.create_action('maximized')).not.toBeNull();
+
+            expect(settings.settings_schema.get_key('fullscreen')).not.toBeNull();
+
+            const sub = settings.get_child('sub');
+            expect(sub.get_uint('marine')).toEqual(10);
+        });
+    });
+});
diff --git a/modules/overrides/Gio.js b/modules/overrides/Gio.js
index 4a0ff00d..36a0ea00 100644
--- a/modules/overrides/Gio.js
+++ b/modules/overrides/Gio.js
@@ -490,4 +490,98 @@ function _init() {
 
     // Temporary Gio.File.prototype fix
     Gio._LocalFilePrototype = Gio.File.new_for_path('').constructor.prototype;
+
+    // Override Gio.Settings and Gio.SettingsSchema - the C API asserts if
+    // trying to access a nonexistent schema or key, which is not handy for
+    // shell-extension writers
+
+    Gio.SettingsSchema.prototype._realGetKey = Gio.SettingsSchema.prototype.get_key;
+    Gio.SettingsSchema.prototype.get_key = function(key) {
+        if (!this.has_key(key))
+            throw new Error(`GSettings key ${key} not found in schema ${this.get_id()}`);
+        return this._realGetKey(key);
+    };
+
+    Gio.Settings.prototype._realMethods = Object.assign({}, Gio.Settings.prototype);
+
+    function createCheckedMethod(method, checkMethod = '_checkKey') {
+        return function(id, ...args) {
+            this[checkMethod](id);
+            return this._realMethods[method].call(this, id, ...args);
+        };
+    }
+
+    Object.assign(Gio.Settings.prototype, {
+        _realInit: Gio.Settings.prototype._init,  // add manually, not enumerable
+        _init(props = {}) {
+            // 'schema' is a deprecated alias for schema_id
+            const requiredProps = ['schema', 'schema-id', 'schema_id', 'schemaId',
+                'settings-schema', 'settings_schema', 'settingsSchema'];
+            if (requiredProps.every(prop => !(prop in props)))
+                throw new Error("One of property 'schema-id' or " +
+                    "'settings-schema' are required for Gio.Settings");
+
+            const checkSchemasProps = ['schema', 'schema-id', 'schema_id', 'schemaId'];
+            const source = Gio.SettingsSchemaSource.get_default();
+            for (const prop of checkSchemasProps) {
+                if (!(prop in props))
+                    continue;
+                if (source.lookup(props[prop], true) === null)
+                    throw new Error(`GSettings schema ${props[prop]} not found`);
+            }
+
+            return this._realInit(props);
+        },
+
+        _checkKey(key) {
+            // Avoid using has_key(); checking a JS array is faster than calling
+            // through G-I.
+            if (!this._keys)
+                this._keys = this.settings_schema.list_keys();
+
+            if (!this._keys.includes(key))
+                throw new Error(`GSettings key ${key} not found in schema ${this.schema_id}`);
+        },
+
+        _checkChild(name) {
+            if (!this._children)
+                this._children = this.list_children();
+
+            if (!this._children.includes(name))
+                throw new Error(`Child ${name} not found in GSettings schema ${this.schema_id}`);
+        },
+
+        get_boolean: createCheckedMethod('get_boolean'),
+        set_boolean: createCheckedMethod('set_boolean'),
+        get_double: createCheckedMethod('get_double'),
+        set_double: createCheckedMethod('set_double'),
+        get_enum: createCheckedMethod('get_enum'),
+        set_enum: createCheckedMethod('set_enum'),
+        get_flags: createCheckedMethod('get_flags'),
+        set_flags: createCheckedMethod('set_flags'),
+        get_int: createCheckedMethod('get_int'),
+        set_int: createCheckedMethod('set_int'),
+        get_int64: createCheckedMethod('get_int64'),
+        set_int64: createCheckedMethod('set_int64'),
+        get_string: createCheckedMethod('get_string'),
+        set_string: createCheckedMethod('set_string'),
+        get_strv: createCheckedMethod('get_strv'),
+        set_strv: createCheckedMethod('set_strv'),
+        get_uint: createCheckedMethod('get_uint'),
+        set_uint: createCheckedMethod('set_uint'),
+        get_uint64: createCheckedMethod('get_uint64'),
+        set_uint64: createCheckedMethod('set_uint64'),
+        get_value: createCheckedMethod('get_value'),
+        set_value: createCheckedMethod('set_value'),
+
+        bind: createCheckedMethod('bind'),
+        bind_writable: createCheckedMethod('bind_writable'),
+        create_action: createCheckedMethod('create_action'),
+        get_default_value: createCheckedMethod('get_default_value'),
+        get_user_value: createCheckedMethod('get_user_value'),
+        is_writable: createCheckedMethod('is_writable'),
+        reset: createCheckedMethod('reset'),
+
+        get_child: createCheckedMethod('get_child', '_checkChild'),
+    });
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]