1/* ds-registry.vala
2 *
3 * Copyright © 2011 Collabora Ltd.
4 *             By Siegfried-Angel Gevatter Pujals <siegfried@gevatter.com>
5 * Copyright © 2011 Stefano Candori <stefano.candori@gmail.com>
6 * Copyright © 2012 Canonical Ltd.
7 *             By Siegfried-A. Gevatter <siegfried.gevatter@collabora.co.uk>
8 *
9 * Based upon a Python implementation:
10 *  Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com>
11 *  Copyright © 2011 Canonical Ltd.
12 *
13 * This program is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU Lesser General Public License as published by
15 * the Free Software Foundation, either version 2.1 of the License, or
16 * (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU Lesser General Public License
24 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
25 *
26 */
27
28using Zeitgeist;
29
30namespace Zeitgeist
31{
32    [DBus (name = "org.gnome.zeitgeist.StorageMonitor")]
33    public interface RemoteStorageMonitor: Object
34    {
35        [DBus (signature = "a(sa{sv})")]
36        public abstract Variant get_storages () throws Error;
37
38        public signal void storage_available (string storage_id,
39            [DBus (signature = "a{sv}")] Variant storage_description);
40        public signal void storage_unavailable (string storage_id);
41    }
42
43    public interface NetworkMonitor: Object
44    {
45        // This method emits the on_network_up/on_network_up signals
46        // basing on the initial state of the network.
47        public abstract void setup ();
48
49        public signal void on_network_up ();
50        public signal void on_network_down ();
51    }
52
53    namespace StorageMedia
54    {
55        private Variant to_variant (string medium_name, bool available,
56            string icon, string display_name)
57        {
58            var vb = new VariantBuilder (new VariantType ("(sa{sv})"));
59
60            vb.add ("s", medium_name);
61            vb.open (new VariantType ("a{sv}"));
62            {
63                vb.open (new VariantType ("{sv}"));
64                vb.add ("s", "available");
65                vb.add ("v", new Variant ("b", available));
66                vb.close ();
67                vb.open (new VariantType ("{sv}"));
68                vb.add ("s", "icon");
69                vb.add ("v", new Variant ("s", icon));
70                vb.close ();
71                vb.open (new VariantType ("{sv}"));
72                vb.add ("s", "display-name");
73                vb.add ("v", new Variant ("s", display_name));
74                vb.close ();
75            }
76            vb.close ();
77
78            return vb.end ();
79        }
80    }
81
82    /*
83     * The Storage Monitor monitors the availability of network interfaces
84     * and storage devices (USB drives, data/audio/video CD/DVDs, etc) and
85     * updates the Zeitgeist database with this information so clients can
86     * efficiently query based on the storage identifier and availability
87     * of the storage medium the event subjects reside on.
88     *
89     * Subject can have the following types of storage identifiers:
90     *  - for local resources, the fixed identifier `local`;
91     *  - for network URIs, the fixed identifier `net`;
92     *  - for resources on storage devices, the UUID of the partition
93     *    they reside in;
94     *  - otherwise, the fixed identifier `unknown`.
95     *
96     * Subjects with storage `local` or `unknown` are always considered as
97     * available; for network resources, the monitor will use either ConnMan
98     * or NetworkManager (whichever is available).
99     *
100     * For subjects being inserted without a storage id set, this extension
101     * will attempt to determine it and update the subject on the fly.
102     */
103    class StorageMonitor: Extension, RemoteStorageMonitor
104    {
105        private const string[] network_uri_schemes = {
106            "dav", "davs", "ftp", "http", "https", "mailto",
107            "sftp", "smb", "ssh" };
108
109        private Zeitgeist.SQLite.Database database;
110        private unowned Sqlite.Database db;
111        private uint registration_id;
112
113        private Sqlite.Statement get_storages_stmt;
114        private Sqlite.Statement store_storage_medium_stmt;
115        private Sqlite.Statement update_storage_medium_stmt;
116        private Sqlite.Statement insert_unavailable_medium_stmt;
117        private Sqlite.Statement update_medium_state_stmt;
118
119        private NetworkMonitor network;
120        private uint watch_connman;
121        private uint watch_nm;
122
123        StorageMonitor ()
124        {
125            Object ();
126        }
127
128        construct
129        {
130            try
131            {
132                prepare_queries ();
133            }
134            catch (EngineError e)
135            {
136                warning ("Storage Monitor couldn't communicate with DB - bye");
137                return;
138            }
139
140            // This will be called after bus is acquired, so it shouldn't block
141            try
142            {
143                var connection = Bus.get_sync (BusType.SESSION, null);
144                registration_id = connection.register_object<RemoteStorageMonitor> (
145                    "/org/gnome/zeitgeist/storagemonitor", this);
146            }
147            catch (Error err)
148            {
149                warning ("%s", err.message);
150            }
151
152            /*
153             * This is disabled because it causes races on some hardware
154             * (zg will be using 100% cpu, maybe eat up lots of memory etc.)
155             */
156            /*
157            VolumeMonitor monitor = VolumeMonitor.get ();
158            monitor.volume_added.connect (on_volume_added);
159            monitor.volume_removed.connect (on_volume_removed);
160            foreach (Volume volume in monitor.get_volumes ())
161            {
162                add_storage_medium (get_volume_id (volume),
163                    volume.get_icon ().to_string (), volume.get_name ());
164            }
165            */
166
167            // Dynamically decide whether to use Connman or NetworkManager
168            watch_connman = Bus.watch_name (BusType.SYSTEM,
169                                      "net.connman",
170                                      BusNameWatcherFlags.NONE,
171                                      name_appeared_handler,
172                                      null);
173            watch_nm = Bus.watch_name (BusType.SYSTEM,
174                                      "org.freedesktop.NetworkManager",
175                                      BusNameWatcherFlags.NONE,
176                                      name_appeared_handler,
177                                      null);
178
179        }
180
181        private void name_appeared_handler (DBusConnection connection, string name, string name_owner)
182        {
183            if (this.network != null)
184                return;
185
186            if (name == "net.connman")
187                this.network = new ConnmanNetworkMonitor ();
188            else if (name == "org.freedesktop.NetworkManager")
189                this.network = new NMNetworkMonitor ();
190
191            this.network.on_network_up.connect (() =>
192                this.add_storage_medium ("net", "stock_internet", "Internet"));
193            this.network.on_network_down.connect (() =>
194                this.remove_storage_medium ("net"));
195
196            this.network.setup ();
197
198            Bus.unwatch_name (watch_connman);
199            Bus.unwatch_name (watch_nm);
200        }
201
202        public override void unload ()
203        {
204            // FIXME: move all this D-Bus stuff to some shared
205            // {request,release}_iface functions
206            try
207            {
208                var connection = Bus.get_sync (BusType.SESSION, null);
209                if (registration_id != 0)
210                {
211                    connection.unregister_object (registration_id);
212                    registration_id = 0;
213                }
214            }
215            catch (Error err)
216            {
217                warning ("%s", err.message);
218            }
219
220            debug ("%s, this.ref_count = %u", GLib.Log.METHOD, this.ref_count);
221        }
222
223        private void prepare_queries () throws EngineError
224        {
225            database = engine.database;
226            db = database.database;
227
228            int rc;
229            string sql;
230
231            // Prepare query to retrieve all storage medium information
232            sql = """
233                SELECT value, state, icon, display_name
234                FROM storage
235                """;
236            rc = db.prepare_v2 (sql, -1, out get_storages_stmt);
237            database.assert_query_success (rc, "Storage retrieval query error");
238
239            sql = """
240                INSERT INTO storage (
241                    value, state, icon, display_name
242                ) VALUES (
243                    ?, ?, ?, ?
244                )""";
245            rc = db.prepare_v2 (sql, -1, out store_storage_medium_stmt);
246            database.assert_query_success (rc, "Storage insertion query error");
247
248            sql = """
249                UPDATE storage SET
250                state=?, icon=?, display_name=?
251                WHERE value=?
252                """;
253            rc = db.prepare_v2 (sql, -1, out update_storage_medium_stmt);
254            database.assert_query_success (rc, "Storage update query error");
255
256            sql = """
257                INSERT INTO storage (
258                    state, value
259                ) VALUES (
260                    ?, ?
261                )""";
262            rc = db.prepare_v2 (sql, -1, out insert_unavailable_medium_stmt);
263            database.assert_query_success (rc,
264                "insert_unavailable_medium_stmt error");
265
266            sql = """
267                UPDATE storage
268                SET state=?
269                WHERE value=?
270                """;
271            rc = db.prepare_v2 (sql, -1, out update_medium_state_stmt);
272            database.assert_query_success (rc,
273                "update_medium_state_stmt error");
274        }
275
276        public override void pre_insert_events (GenericArray<Event?> events,
277            BusName? sender)
278        {
279            for (int i = 0; i < events.length; ++i)
280            {
281                if (events[i] == null) continue;
282                for (int j = 0; j < events[i].subjects.length; ++j)
283                {
284                    Subject subject = events[i].subjects[j];
285                    if (Utils.is_empty_string (subject.storage))
286                        subject.storage = find_storage_for_uri (subject.uri);
287                }
288            }
289        }
290
291        /*
292         * Find the name of the storage medium the given URI resides on.
293         */
294        private string find_storage_for_uri (string uri)
295        {
296            File file = File.new_for_uri (uri);
297            string uri_scheme = file.get_uri_scheme ();
298            /*
299            // FIXME: uncomment this once gvfs is our friend again
300            if (uri_scheme == "file")
301            {
302                try
303                {
304                    Mount mount = file.find_enclosing_mount ();
305                    return get_volume_id (mount.get_volume ());
306                }
307                catch (Error err)
308                {
309                    return "local";
310                }
311            }
312            else*/
313            if (uri_scheme in network_uri_schemes)
314            {
315                return "net";
316            }
317
318            return "unknown";
319        }
320
321        /*
322        // It is not being used since gvfs is not being friendly
323        private void on_volume_added (Volume volume)
324        {
325            debug ("volume added");
326            Icon icon = volume.get_icon ();
327            string icon_name = "";
328            // FIXME: why volume.get_icon ().to_string () above but not here?
329            if (icon is ThemedIcon && ((ThemedIcon) icon).get_names ().length > 0)
330                icon_name = ((ThemedIcon) icon).get_names ()[0];
331            add_storage_medium (get_volume_id (volume), icon_name,
332                volume.get_name ());
333        }
334
335        private void on_volume_removed (Volume volume)
336        {
337            debug ("Volume removed");
338            remove_storage_medium (get_volume_id (volume));
339        }
340        */
341
342        /*
343         * Return a string identifier for a GIO Volume. This id is constructed
344         * as a `best effort` since we can not always uniquely identify
345         * volumes, especially audio- and data CDs are problematic.
346         */
347
348        /*private string get_volume_id (Volume volume)
349        {
350            string volume_id;
351
352            volume_id = volume.get_uuid ();
353            if (volume_id != null)
354                return volume_id;
355
356            volume_id = volume.get_identifier ("uuid");
357            if (volume_id != null)
358                return volume_id;
359
360            volume_id = volume.get_identifier ("label");
361            if (volume_id != null)
362                return volume_id;
363
364            return "unknown";
365        }*/
366
367        public void add_storage_medium (string medium_name, string icon,
368            string display_name)
369        {
370            debug ("VOLUME ADDED: %s".printf(medium_name));
371            store_storage_medium_stmt.reset ();
372            store_storage_medium_stmt.bind_text (1, medium_name);
373            store_storage_medium_stmt.bind_int (2, 1);
374            store_storage_medium_stmt.bind_text (3, icon);
375            store_storage_medium_stmt.bind_text (4, display_name);
376            if (store_storage_medium_stmt.step () != Sqlite.DONE)
377            {
378                update_storage_medium_stmt.reset ();
379                update_storage_medium_stmt.bind_int (1, 1);
380                update_storage_medium_stmt.bind_text (2, icon);
381                update_storage_medium_stmt.bind_text (3, display_name);
382                update_storage_medium_stmt.bind_text (4, medium_name);
383                int rc = update_storage_medium_stmt.step ();
384                try
385                {
386                    database.assert_query_success (rc, "add_storage_medium", Sqlite.DONE);
387                }
388                catch (EngineError e)
389                {
390                    warning ("Could not add storage medium: %s", e.message);
391                }
392            }
393            storage_available (medium_name, StorageMedia.to_variant (
394                medium_name, true, icon, display_name));
395        }
396
397        public void remove_storage_medium (string medium_name)
398        {
399            debug ("VOLUME REMOVED: %s".printf(medium_name));
400            insert_unavailable_medium_stmt.reset ();
401            insert_unavailable_medium_stmt.bind_int (1, 0);
402            insert_unavailable_medium_stmt.bind_text (2, medium_name);
403            if (insert_unavailable_medium_stmt.step () != Sqlite.DONE)
404            {
405                update_medium_state_stmt.reset ();
406                update_medium_state_stmt.bind_int (1, 0);
407                update_medium_state_stmt.bind_text (2, medium_name);
408                int rc = update_medium_state_stmt.step ();
409                try
410                {
411                    database.assert_query_success (rc, "remove_storage_medium",
412                        Sqlite.DONE);
413                }
414                catch (EngineError e)
415                {
416                    warning ("Could not remove storage medium: %s", e.message);
417                }
418            }
419            storage_unavailable (medium_name);
420        }
421
422        public Variant get_storages () throws EngineError
423        {
424            var vb = new VariantBuilder (new VariantType ("a(sa{sv})"));
425
426            int rc;
427            get_storages_stmt.reset ();
428            while ((rc = get_storages_stmt.step ()) == Sqlite.ROW)
429            {
430                // name, available?, icon, display name
431                Variant medium = StorageMedia.to_variant (
432                    get_storages_stmt.column_text (0),
433                    get_storages_stmt.column_int (1) == 1,
434                    get_storages_stmt.column_text (2) ?? "",
435                    get_storages_stmt.column_text (3) ?? "");
436                vb.add_value (medium);
437            }
438            database.assert_query_success (rc, "get_storages", Sqlite.DONE);
439
440            return vb.end ();
441        }
442
443    }
444
445    /*
446     * Monitor the availability of working network connections using
447     *  Network Manager (requires 0.8 or later).
448     * See http://projects.gnome.org/NetworkManager/developers/spec-08.html
449     */
450    private class NMNetworkMonitor : Object, NetworkMonitor
451    {
452        private const string NM_BUS_NAME = "org.freedesktop.NetworkManager";
453        private const string NM_IFACE = "org.freedesktop.NetworkManager";
454        private const string NM_OBJECT_PATH = "/org/freedesktop/NetworkManager";
455
456        // NM 0.9 broke API so we have to check for two possible values for the state
457        private const int NM_STATE_CONNECTED_PRE_09 = 3;
458        private const int NM_STATE_CONNECTED_POST_09 = 70;
459
460        private NetworkManagerDBus proxy;
461
462        public NMNetworkMonitor ()
463        {
464            Object ();
465        }
466
467        public void setup ()
468        {
469            debug ("Creating NetworkManager network monitor");
470            try
471            {
472                proxy = Bus.get_proxy_sync<NetworkManagerDBus> (BusType.SYSTEM,
473                                            NM_BUS_NAME,
474                                            NM_OBJECT_PATH);
475                proxy.state_changed.connect (this.on_state_changed);
476
477                uint32 state = proxy.state ();
478                this.on_state_changed (state);
479            }
480            catch (IOError e )
481            {
482                warning ("%s", e.message);
483            }
484        }
485
486        private void on_state_changed(uint32 state)
487        {
488            debug ("NetworkManager network state: %u", state);
489            if (state == NMNetworkMonitor.NM_STATE_CONNECTED_PRE_09 ||
490                state == NMNetworkMonitor.NM_STATE_CONNECTED_POST_09)
491                on_network_up ();
492            else
493                on_network_down ();
494        }
495    }
496
497    private class ConnmanNetworkMonitor : Object, NetworkMonitor
498    {
499        private const string CM_BUS_NAME = "net.connman";
500        private const string CM_IFACE = "net.connman.Manager";
501        private const string CM_OBJECT_PATH = "/";
502
503        private ConnmanManagerDBus proxy;
504
505        public ConnmanNetworkMonitor ()
506        {
507            Object ();
508        }
509
510        public void setup ()
511        {
512            debug ("Creating ConnmanNetworkManager network monitor");
513
514            try
515            {
516                proxy = Bus.get_proxy_sync<ConnmanManagerDBus> (
517                    BusType.SYSTEM, CM_BUS_NAME, CM_OBJECT_PATH);
518
519                // There is a bug in some Connman versions causing it
520                // to not emit the net.connman.Manager.StateChanged
521                // signal. We take our chances this instance is working
522                // properly :-)
523                proxy.state_changed.connect (this.on_state_changed);
524
525                string state = proxy.get_state ();
526                this.on_state_changed (state);
527            }
528            catch (IOError e )
529            {
530                warning ("%s", e.message);
531            }
532        }
533
534        private void on_state_changed(string state)
535        {
536            debug ("ConnmanNetworkMonitor network state: %s", state);
537            if (state == "online")
538                on_network_up ();
539            else
540                on_network_down ();
541        }
542    }
543
544    [ModuleInit]
545#if BUILTIN_EXTENSIONS
546    public static Type storage_monitor_init (TypeModule module)
547    {
548#else
549    public static Type extension_register (TypeModule module)
550    {
551#endif
552        return typeof (StorageMonitor);
553    }
554}
555
556// vim:expandtab:ts=4:sw=4
557