Merge lp:~jeremy-munsch/synapse-project/virtualbox-plugin into lp:synapse-project

Proposed by Jeremy Munsch on 2015-11-21
Status: Needs review
Proposed branch: lp:~jeremy-munsch/synapse-project/virtualbox-plugin
Merge into: lp:synapse-project
Diff against target: 499 lines (+443/-1)
5 files modified
configure.ac (+3/-1)
debian/control (+1/-0)
src/plugins/Makefile.am (+1/-0)
src/plugins/virtualbox-plugin.vala (+437/-0)
src/ui/synapse-main.vala (+1/-0)
To merge this branch: bzr merge lp:~jeremy-munsch/synapse-project/virtualbox-plugin
Reviewer Review Type Date Requested Status
Rico Tzschichholz 2015-11-21 Pending
Review via email: mp+278244@code.launchpad.net

Description of the change

Search for virtual machines and provides actions for it
using libxmlbird since librest xml is buggy/harassing to work with.

To post a comment you must log in.
634. By Rico Tzschichholz on 2015-11-30

ui/controller: Some optimization accessing model.query[] in search*()

635. By Rico Tzschichholz on 2015-11-30

core/query: Cache and reuse Regex.escape_string (query)

636. By Rico Tzschichholz on 2015-11-30

debian: Bump build-deps accordingly

637. By Rico Tzschichholz on 2016-02-26

plugins: Add suport for MATE Screensaver

638. By Rico Tzschichholz on 2016-02-26

po: Update translations

639. By Rico Tzschichholz on 2016-02-27

desktop-file-plugin: correct ApplicationsMatch to DesktopFileMatch

640. By Rico Tzschichholz on 2016-02-27

src: Drop "using Gee;"

641. By Rico Tzschichholz on 2016-02-27

src: Drop "using Json;"

642. By Rico Tzschichholz on 2016-02-27

src: Drop "using Zeitgeist;"

643. By Rico Tzschichholz on 2016-02-27

src: Drop "using Gtk;"

644. By Rico Tzschichholz on 2016-02-27

src: Drop "using Cairo;"

645. By Rico Tzschichholz on 2016-02-27

src: Drop last usages of "using *;"

646. By Rico Tzschichholz on 2016-02-27

ui: Show release-name in about-dialog only

647. By Rico Tzschichholz on 2016-02-27

ui: Bump to 2016

648. By Rico Tzschichholz on 2016-02-28

Release 0.2.99.2

649. By Rico Tzschichholz on 2016-03-02

ui: Set and update client-window of Gtk.IMMulticontext

650. By Rico Tzschichholz on 2016-03-08

main: Properly construct OptionEntry

Stop using an empty string where is getting treated as translateable.

651. By Michael Aquilina on 2016-04-01

Add pass plugin

652. By Rico Tzschichholz on 2016-09-29

Fix non-matching generic types

653. By Rico Tzschichholz on 2016-10-06

Fix non-matching generic types of interface implementations

654. By Jeremy Munsch on 2016-12-16

add virtualbox plugin
add libxmlbird dependency

Unmerged revisions

654. By Jeremy Munsch on 2016-12-16

add virtualbox plugin
add libxmlbird dependency

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configure.ac'
2--- configure.ac 2016-02-28 12:40:08 +0000
3+++ configure.ac 2016-12-16 21:07:04 +0000
4@@ -62,7 +62,8 @@
5 gee-0.8 >= $MIN_GEE_VERSION \
6 json-glib-1.0 >= $MIN_JSON_VERSION \
7 keybinder-3.0 \
8- libnotify
9+ libnotify \
10+ xmlbird
11 )
12 SYNAPSE_MODULES_VALAFLAGS=" --pkg gdk-x11-3.0 \
13 --pkg gtk+-3.0 \
14@@ -71,6 +72,7 @@
15 --pkg gee-0.8 \
16 --pkg json-glib-1.0 \
17 --pkg keybinder-3.0 \
18+ --pkg xmlbird \
19 --pkg libnotify"
20 AC_SUBST(SYNAPSE_MODULES_VALAFLAGS)
21
22
23=== modified file 'debian/control'
24--- debian/control 2015-11-30 14:47:10 +0000
25+++ debian/control 2016-12-16 21:07:04 +0000
26@@ -15,6 +15,7 @@
27 libkeybinder-3.0-dev,
28 libnotify-dev,
29 librest-dev,
30+ libxmlbird-dev,
31 libappindicator3-dev (>= 0.0.7)
32 Vcs-Bzr: http://bazaar.launchpad.net/~synapse-core/synapse-project/trunk/
33 Vcs-Browser: http://bazaar.launchpad.net/~synapse-core/synapse-project/trunk/files
34
35=== modified file 'src/plugins/Makefile.am'
36--- src/plugins/Makefile.am 2016-04-01 05:58:51 +0000
37+++ src/plugins/Makefile.am 2016-12-16 21:07:04 +0000
38@@ -53,6 +53,7 @@
39 test-slow-plugin.vala \
40 xnoise-media-player-plugin.vala \
41 system-management.vala \
42+ virtualbox-plugin.vala \
43 zeal-plugin.vala \
44 $(NULL)
45
46
47=== added file 'src/plugins/virtualbox-plugin.vala'
48--- src/plugins/virtualbox-plugin.vala 1970-01-01 00:00:00 +0000
49+++ src/plugins/virtualbox-plugin.vala 2016-12-16 21:07:04 +0000
50@@ -0,0 +1,437 @@
51+/*
52+ * Copyright (C) 2015 Jérémy Munsch <jeremy.munsch@gmail.com>
53+ *
54+ * This program is free software; you can redistribute it and/or modify
55+ * it under the terms of the GNU General Public License as published by
56+ * the Free Software Foundation; either version 2 of the License, or
57+ * (at your option) any later version.
58+ *
59+ * This program is distributed in the hope that it will be useful,
60+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
61+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
62+ * GNU General Public License for more details.
63+ *
64+ * You should have received a copy of the GNU General Public License
65+ * along with this program; if not, write to the Free Software
66+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
67+ *
68+ * Note :
69+ * Using libxmlbird since librest is buggy/pain to use
70+ *
71+ * What is does :
72+ * Search hosts virtual machines and provides
73+ * some actions took from virtualbox --help
74+ */
75+
76+using B;
77+
78+namespace Synapse
79+{
80+ private errordomain VMParseError { ERROR }
81+
82+ public class VirtualBoxPlugin : Object, Activatable, ItemProvider, ActionProvider
83+ {
84+ public bool enabled { get; set; default = true; }
85+
86+ public static string virtualbox_config_dir { get; private set; default = "VirtualBox"; }
87+ public static string virtualbox_vm_dir { get; private set; }
88+
89+ private Gee.List<VirtualBoxMachineMatch> machines;
90+ private Gee.List<VirtualBoxAction> actions;
91+
92+ public void activate ()
93+ {
94+ parse_vm_config.begin ();
95+ }
96+
97+ public void deactivate ()
98+ {
99+ }
100+
101+ static void register_plugin ()
102+ {
103+ PluginRegistry.get_default ().register_plugin (
104+ typeof (VirtualBoxPlugin),
105+ _("VirtualBox"),
106+ _("Control you virtual machines"),
107+ "virtualbox",
108+ register_plugin,
109+ Environment.find_program_in_path ("virtualbox") != null,
110+ _("Unable to find \"virtualbox\" program")
111+ );
112+ }
113+
114+ static construct
115+ {
116+ register_plugin ();
117+ virtualbox_config_dir = Path.build_filename (Environment.get_user_config_dir (), virtualbox_config_dir);
118+ }
119+
120+ construct
121+ {
122+ machines = new Gee.ArrayList<VirtualBoxMachineMatch> ();
123+ actions = new Gee.ArrayList<VirtualBoxAction> ();
124+ actions.add (new StartVirtualMachine ());
125+ actions.add (new StartFullscreenVirtualMachine ());
126+ actions.add (new StartSeemlessVirtualMachine ());
127+ actions.add (new StartScaleVirtualMachine ());
128+ actions.add (new RestoreVirtualMachine ());
129+ actions.add (new StartPausedVirtualMachine ());
130+ actions.add (new StartSeparateVirtualMachine ());
131+ }
132+
133+ public bool handles_query (Query query)
134+ {
135+ return (QueryFlags.APPLICATIONS in query.query_type);
136+ }
137+
138+ public async ResultSet? search (Query q) throws SearchError
139+ {
140+ var results = new ResultSet ();
141+
142+ var matchers = Query.get_matchers_for_query (q.query_string, MatcherFlags.NO_FUZZY, RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS);
143+ foreach (var matcher in matchers)
144+ {
145+ foreach (var machine in machines)
146+ {
147+ q.check_cancellable ();
148+ if (matcher.key.match (machine.title))
149+ {
150+ results.add (machine, matcher.value);
151+ }
152+ }
153+ }
154+ return results;
155+ }
156+
157+ public ResultSet? find_for_match (ref Query q, Match match)
158+ {
159+ if (!actions[0].valid_for_match (match)) return null;
160+
161+ var matchers = Query.get_matchers_for_query (q.query_string, 0, RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS);
162+
163+ var results = new ResultSet ();
164+ foreach (var matcher in matchers)
165+ {
166+ foreach (var action in actions)
167+ {
168+ if (matcher.key.match (action.title))
169+ {
170+ int relevancy = (match is StartVirtualMachine) ?
171+ action.default_relevancy :
172+ action.default_relevancy - MatchScore.INCREMENT_MINOR;
173+ results.add (action, relevancy);
174+ }
175+ }
176+ }
177+ return results;
178+ }
179+
180+
181+ /**
182+ * Actions
183+ */
184+
185+ private abstract class VirtualBoxAction : Action
186+ {
187+ public abstract string command { get; set; }
188+ public new virtual int default_relevancy { get; set; default = MatchScore.AVERAGE; }
189+
190+ public override bool valid_for_match (Match match)
191+ {
192+ return (match is VirtualBoxMachineMatch);
193+ }
194+
195+ public override void do_execute (Match match, Match? target = null)
196+ {
197+ string cmd = command.printf (((VirtualBoxMachineMatch)match).uuid);
198+ try
199+ {
200+ AppInfo ai = AppInfo.create_from_commandline (cmd, "virtualbox", 0);
201+ if (!ai.launch (null, null))
202+ warning ("Could not launch VirtualBox %s".printf (cmd));
203+ }
204+ catch (Error err)
205+ {
206+ warning ("Could not launch %s %s", cmd, err.message);
207+ }
208+ }
209+ }
210+
211+ private class StartVirtualMachine : VirtualBoxAction
212+ {
213+ public override string command { get; set; default = "virtualbox --startvm %s"; }
214+ public StartVirtualMachine ()
215+ {
216+ Object (title: _("Turn on"),
217+ description: _("Start the virtual machine"),
218+ icon_name: "virtualbox",
219+ has_thumbnail: false);
220+ default_relevancy = MatchScore.HIGHEST;
221+ }
222+ }
223+
224+ private class StartFullscreenVirtualMachine : VirtualBoxAction
225+ {
226+ public override string command { get; set; default = "virtualbox --startvm %s --fullscreen"; }
227+ public StartFullscreenVirtualMachine ()
228+ {
229+ Object (title: _("Fullscreen"),
230+ description: _("Start the virtual machine in fullscreen mode"),
231+ icon_name: "virtualbox",
232+ has_thumbnail: true);
233+ }
234+ }
235+
236+ private class StartSeemlessVirtualMachine : VirtualBoxAction
237+ {
238+ public override string command { get; set; default = "virtualbox --startvm %s --seamless"; }
239+ public StartSeemlessVirtualMachine ()
240+ {
241+ Object (title: _("Seamless"),
242+ description: _("Start the virtual machine in seamless mode"),
243+ icon_name: "virtualbox",
244+ has_thumbnail: false);
245+ }
246+ }
247+
248+ private class StartScaleVirtualMachine : VirtualBoxAction
249+ {
250+ public override string command { get; set; default = "virtualbox --startvm %s --scale"; }
251+ public StartScaleVirtualMachine ()
252+ {
253+ Object (title: _("Scale"),
254+ description: _("Start the virtual machine in scale mode"),
255+ icon_name: "virtualbox",
256+ has_thumbnail: false);
257+ }
258+ }
259+
260+ private class RestoreVirtualMachine : VirtualBoxAction
261+ {
262+ public override string command { get; set; default = "virtualbox --startvm %s --restore"; }
263+ public RestoreVirtualMachine ()
264+ {
265+ Object (title: _("Restore"),
266+ description: _("Restore the last vitual machine state"),
267+ icon_name: "virtualbox",
268+ has_thumbnail: false);
269+ }
270+ }
271+
272+ private class StartPausedVirtualMachine : VirtualBoxAction
273+ {
274+ public override string command { get; set; default = "virtualbox --startvm %s --start-paused"; }
275+ public StartPausedVirtualMachine ()
276+ {
277+ Object (title: _("Start Paused"),
278+ description: _("Start the virtual machine in paused state"),
279+ icon_name: "virtualbox",
280+ has_thumbnail: false);
281+ }
282+ }
283+
284+ private class StartSeparateVirtualMachine : VirtualBoxAction
285+ {
286+ public override string command { get; set; default = "virtualbox --startvm %s --separate"; }
287+ public StartSeparateVirtualMachine ()
288+ {
289+ Object (title: _("Start Separated"),
290+ description: _("Start the virtual machine in a separate process"),
291+ icon_name: "virtualbox",
292+ has_thumbnail: false);
293+ }
294+ }
295+
296+
297+ /**
298+ * Match
299+ */
300+
301+ private class VirtualBoxMachineMatch : Match
302+ {
303+ public string name { get; set; }
304+ public string uuid { get; set; }
305+ public string src { get; set; }
306+ public string os_type { get; set; }
307+
308+ public VirtualBoxMachineMatch (string name, string uuid, string src, string os_type)
309+ {
310+ Object (title: "VM %s %s".printf (name, os_type),
311+ description : "",
312+ icon_name: "virtualbox",
313+ has_thumbnail: false);
314+ this.uuid = uuid;
315+ this.src = src;
316+ this.os_type = os_type;
317+ }
318+ }
319+
320+
321+ /**
322+ * private methods
323+ */
324+
325+ private async void parse_vm_config ()
326+ {
327+ try
328+ {
329+ Gee.List<Tag> results = new Gee.ArrayList<Tag> ();
330+ string vm_config_file = Path.build_filename (virtualbox_config_dir, "VirtualBox.xml");
331+
332+ Tag root = parse_xml_file (vm_config_file);
333+
334+ if (null == root)
335+ throw new VMParseError.ERROR ("Failed to parse VirtualBox config file %s".printf (vm_config_file));
336+
337+ find_all_tags ("MachineEntry", root, ref results);
338+
339+ foreach (var machine_entry in results)
340+ {
341+ string uuid = get_attribute ("uuid", machine_entry) ?? null;
342+ string src = get_attribute ("src", machine_entry) ?? null;
343+
344+ if (null == uuid || null == src)
345+ throw new VMParseError.ERROR ("Could not get uuid %s or src %s from MachineEntry %s".printf (uuid, src, vm_config_file));
346+
347+ Idle.add_full (Priority.DEFAULT_IDLE, parse_vm_config.callback);
348+ yield;
349+
350+ Tag machine_tag = parse_xml_file (src);
351+
352+ if (null == machine_tag)
353+ {
354+ message ("Failed to parse VirtualBox config file %s".printf (src));
355+ continue;
356+ }
357+
358+ machine_tag = find_first_tag ("Machine", machine_tag);
359+
360+ if (null == machine_tag)
361+ {
362+ message ("Could not get Machine from %s".printf (src));
363+ continue;
364+ }
365+
366+ string name = get_attribute ("name", machine_tag) ?? null;
367+ string os_type = get_attribute ("OSType", machine_tag) ?? null;
368+
369+ if (null == name || null == os_type)
370+ {
371+ message ("Could not get name %s or ostype %s from MachineEntry %s".printf (name, os_type, src));
372+ continue;
373+ }
374+
375+ machines.add (new VirtualBoxMachineMatch (name, uuid, src, parse_os (os_type)));
376+ }
377+ }
378+ catch (Error e)
379+ {
380+ warning ("%s".printf (e.message));
381+ }
382+ }
383+
384+ private string parse_os (string os_info)
385+ {
386+ int _pos = os_info.index_of_char ('_');
387+ // 0: arch, 1: os, 2: version
388+ string os[3] = { -1 != _pos ? os_info.slice (_pos, os_info.length).replace ("_", "x") : "",
389+ -1 != _pos ? os_info.slice (0, _pos).replace ("_", "x") : os_info,
390+ "" };
391+ try
392+ {
393+ os[2] = (new Regex ("[a-zA-Z]")).replace (os[1], os[1].length, 0, "");
394+ os[1] = os[1].replace (os[2], "");
395+ switch (os[1])
396+ {
397+ case "Linux":
398+ if (os[2].length > 1) // eg: 22 -> 2.2
399+ os[2] = os[2].substring (0, 1) + "." + os[2].substring (1);
400+ break;
401+ case "MacOS":
402+ if (os[2].length > 2) // eg: 106 -> 10.6
403+ os[2] = os[2].substring (0, 2) + "." + os[2].substring (2);
404+ break;
405+ case "Windows":
406+ if ("81" == os[2])
407+ os[2] = "8.1";
408+ break;
409+ case "L":
410+ os[1] = "L4";
411+ os[2] = "";
412+ break;
413+ case "OSx": // IBM
414+ os[1] = "OS/x";
415+ os[2] = "21";
416+ break;
417+ case "OS2Warp45": // IBM
418+ os[1] = "OS/2 Warp";
419+ os[2] = "45";
420+ break;
421+ case "OS2Warp4": // IBM
422+ os[1] = "OS/2 Warp";
423+ os[2] = "4";
424+ break;
425+ case "OSeCS": // IBM
426+ os[1] = "OS/2 eCS";
427+ os[2] = "";
428+ break;
429+ }
430+ }
431+ catch (Error e)
432+ {
433+ warning ("%s".printf (e.message));
434+ }
435+ return "%s %s %s%s".printf (os[1], os[2], string.nfill (3-os[0].length, ' '), os[0]);
436+ }
437+
438+ /**
439+ * Xml methods
440+ */
441+
442+ private void find_all_tags (string tag_name, Tag root, ref Gee.List<Tag> results)
443+ {
444+ foreach (Tag tag in root) {
445+ if (tag_name == tag.get_name ())
446+ results.add (tag);
447+ else
448+ find_all_tags (tag_name, tag, ref results);
449+ }
450+ }
451+
452+ private Tag? find_first_tag (string tag_name, Tag root)
453+ {
454+ foreach (Tag tag in root) {
455+ if (tag_name == tag.get_name ())
456+ return tag;
457+ else
458+ find_first_tag (tag_name, tag);
459+ }
460+ return null;
461+ }
462+
463+ private string? get_attribute (string attribute_name, Tag tag) {
464+ foreach (Attribute attribute in tag.get_attributes ()) {
465+ if (attribute_name == attribute.get_name ())
466+ return attribute.get_content ();
467+ }
468+ return null;
469+ }
470+
471+ private Tag parse_xml_file (string file) throws FileError, VMParseError
472+ {
473+ size_t length;
474+ string read;
475+ FileUtils.get_contents (file, out read, out length);
476+
477+ XmlParser parser = new XmlParser (read);
478+
479+ if (!parser.validate ())
480+ {
481+ throw new VMParseError.ERROR ("Invalid XML %s".printf (file));
482+ }
483+
484+ return parser.get_root_tag ();
485+ }
486+ }
487+}
488
489=== modified file 'src/ui/synapse-main.vala'
490--- src/ui/synapse-main.vala 2016-04-01 05:58:51 +0000
491+++ src/ui/synapse-main.vala 2016-12-16 21:07:04 +0000
492@@ -183,6 +183,7 @@
493 #endif
494 #if HAVE_LIBREST
495 typeof (ImgUrPlugin),
496+ typeof (VirtualBoxPlugin),
497 #endif
498 // action-only plugins
499 typeof (DevhelpPlugin),