No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

smd-applet.vala 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. // Released under the terms of GPLv3 or at your option any later version.
  2. // No warranties.
  3. // Copyright Enrico Tassi <gares@fettunta.org>
  4. errordomain Exit { ABORT }
  5. bool verbose = false;
  6. void debug(string message) {
  7. if (verbose) stderr.printf("DEBUG: %s\n",message);
  8. }
  9. // minimalistic NetworkManager interface
  10. [DBus (name = "org.freedesktop.NetworkManager")]
  11. interface NetworkManager : Object {
  12. public signal void state_changed(uint state); // throws IOErrror;
  13. public abstract uint state { owned get; }
  14. }
  15. const string NM_SERVICE = "org.freedesktop.NetworkManager";
  16. const string NM_PATH = "/org/freedesktop/NetworkManager";
  17. // Checks if there is a connection
  18. // 70 is NM_STATE_CONNECTED_GLOBAL in NetworkManager >= 0.9
  19. // 3 is NM_STATE_CONNECTED in NetworkManager < 0.9
  20. // We may want to check for >= 50 or 60, since they are respectively LOCAL
  21. // and SITE connections.
  22. static bool is_nm_connected(uint code) {
  23. return code == 3 || code == 70;
  24. }
  25. // a simple class to pass data from the child process to the
  26. // notifier
  27. class Event {
  28. public string message = null;
  29. public string message_icon = "gtk-about";
  30. public bool enter_network_error_mode = false;
  31. public bool enter_error_mode = false;
  32. public bool transient_error_message = false;
  33. // fields meaningful for the error mode only
  34. public string context = null;
  35. public string cause = null;
  36. public string permissions = null;
  37. public string mail_name = null;
  38. public string mail_body = null;
  39. public Gee.ArrayList<string> commands = null;
  40. // constructors
  41. public static Event error(string account, string host,
  42. string context, string cause, string? permissions, string? mail_name,
  43. string? mail_body, Gee.ArrayList<string> commands) {
  44. var e = new Event();
  45. e.message = "An error occurred, click on the icon for more details";
  46. e.message_icon = "error";
  47. e.enter_error_mode = true;
  48. e.cause = cause;
  49. e.context = context;
  50. e.permissions = permissions;
  51. e.mail_name = mail_name;
  52. e.mail_body = mail_body;
  53. e.commands = commands;
  54. return e;
  55. }
  56. public static Event generic_error(string cause) {
  57. var e = new Event();
  58. e.message = "A failure occurred: "+cause;
  59. e.message_icon = "dialog-warning";
  60. e.transient_error_message = true;
  61. return e;
  62. }
  63. public static Event network_error() {
  64. var e = new Event();
  65. e.message = "A persistent network failure occurred";
  66. e.message_icon = "dialog-warning";
  67. e.enter_network_error_mode = true;
  68. return e;
  69. }
  70. public static Event stats(
  71. string account,string host,int new_mails,int del_mails)
  72. {
  73. string preamble = "Synchronize with %s:\n".printf(account);
  74. var e = new Event();
  75. if (new_mails > 0 && del_mails > 0) {
  76. e.message = "%s%d new messages\n%d deleted messages".
  77. printf(preamble,new_mails,del_mails);
  78. } else if (new_mails > 0) {
  79. e.message = "%s%d new messages".printf(preamble,new_mails);
  80. } else {
  81. e.message = "%s%d deleted messages".printf(preamble,del_mails);
  82. }
  83. return e;
  84. }
  85. public bool is_error_event() {
  86. return (this.enter_error_mode ||
  87. this.enter_network_error_mode ||
  88. this.transient_error_message);
  89. }
  90. }
  91. static const string SMD_LOOP = "/bin/smd-loop";
  92. static const string SMD_PUSH = "/bin/smd-push";
  93. static const string SMD_APPLET_UI = "/share/syncmaildir-applet/smd-applet.ui";
  94. static const string SMD_APPLET_DESKTOP = "/share/applications/smd-applet.desktop";
  95. static const string GNOME_AUTOSTART_DISABLED ="X-GNOME-Autostart-enabled=false";
  96. static string SMD_LOGS_DIR;
  97. static string SMD_LOOP_CFG;
  98. static string SMD_PP_DEF_CFG;
  99. static string XDG_AUTORUN_FILE;
  100. // the main class containing all the data smd-applet will use
  101. class smdApplet : Gtk.Application {
  102. // =================== the constants ===============================
  103. // settings keys
  104. static const string key_newmail = "notify-new-mail";
  105. // paths, set by main() to something that depends on the
  106. // installation path
  107. public static string smd_loop_cmd = null;
  108. public static string smd_applet_ui = null;
  109. public static string smd_push_cmd = null;
  110. public static string smd_applet_desktop = null;
  111. // =================== the data =====================================
  112. // The builder
  113. Gtk.Builder builder = null;
  114. // main widgets
  115. Gtk.StatusIcon si = null;
  116. Gtk.Window win = null;
  117. Gtk.Window err_win = null;
  118. Gtk.Notebook notebook = null;
  119. Gtk.Switch sync_active = null;
  120. // Stuff for logs display
  121. Gtk.ComboBoxText cblogs = null;
  122. Gee.ArrayList<string> lognames = null;
  123. // the gsettings entry point
  124. GLib.Settings settings = null;
  125. // the thread to manage the child smd-loop instance
  126. GLib.Thread<void *> thread = null;
  127. bool thread_die = false;
  128. GLib.Pid pid; // smd-loop pid, initially set to 0
  129. // communication structure between the child process (managed by a thread
  130. // and the notifier timeout handler).
  131. Mutex events_lock = Mutex();
  132. Gee.ArrayList<Event> events = null;
  133. // if the program is stuck
  134. bool error_mode = false;
  135. bool network_error_mode = false;
  136. bool config_wait_mode;
  137. GLib.HashTable<Gtk.Widget,string> command_hash = null;
  138. // dbus connection to NetworkManager
  139. NetworkManager net_manager = null;
  140. // last persistent notification, to avoid its garbage collection
  141. // so that the callback is called when the button is clicked
  142. Notify.Notification notification = null;
  143. bool notification_server_has_persistence = false;
  144. static string menu_ui = """
  145. <interface>
  146. <menu id='app-menu'>
  147. <section>
  148. <item>
  149. <attribute name='label' translatable='yes'>_Quit</attribute>
  150. <attribute name='action'>app.quit</attribute>
  151. </item>
  152. </section>
  153. </menu>
  154. </interface>
  155. """;
  156. // ======================= constructor ================================
  157. // initialize data structures and build gtk+ widgets
  158. public smdApplet() throws Exit {
  159. set_application_id("org.syncmaildir");
  160. try { register(); }
  161. catch (GLib.Error e) { stderr.printf("%s\n",e.message); };
  162. // load the ui file
  163. builder = new Gtk.Builder ();
  164. try { builder.add_from_file (smd_applet_ui); }
  165. catch (GLib.Error e) {
  166. stderr.printf("%s\n",e.message);
  167. throw new Exit.ABORT("Unable to load the ui file");
  168. }
  169. try { builder.add_from_string (menu_ui, menu_ui.length); }
  170. catch (GLib.Error e) {
  171. stderr.printf("%s\n",e.message);
  172. throw new Exit.ABORT("Unable to load the menu ui string");
  173. }
  174. // events queue and mutex
  175. events = new Gee.ArrayList<Event>();
  176. // gsettings
  177. settings = new Settings("org.syncmaildir.applet");
  178. // connect to dbus
  179. try {
  180. net_manager=Bus.get_proxy_sync(BusType.SYSTEM,NM_SERVICE,NM_PATH);
  181. net_manager.state_changed.connect((s) => {
  182. if (is_nm_connected(s)) sync_active.set_active(true);
  183. else sync_active.set_active(false);
  184. });
  185. } catch (GLib.Error e) {
  186. stderr.printf("%s\n",e.message);
  187. net_manager=null;
  188. }
  189. // load widgets and attach callbacks
  190. var simple_mainwin = builder.get_object("wMain") as Gtk.Window;
  191. win = new Gtk.ApplicationWindow(this);
  192. simple_mainwin.get_child().reparent(win);
  193. var w = 0;
  194. var h = 0;
  195. simple_mainwin.get_size_request(out w, out h);
  196. win.set_size_request(w,h);
  197. win.set_title(simple_mainwin.get_title());
  198. err_win = builder.get_object("wError") as Gtk.Window;
  199. var lcopyright = builder.get_object("lCopyright") as Gtk.Label;
  200. lcopyright.set_text("Copyright " + SMDConf.COPYRIGHT);
  201. var logs_vb = builder.get_object("vbLog") as Gtk.Grid;
  202. cblogs = new Gtk.ComboBoxText();
  203. lognames = new Gee.ArrayList<string>();
  204. logs_vb.attach(cblogs,0,0,1,1);
  205. cblogs.show();
  206. update_logcontents();
  207. var bnotify = builder.get_object("sNotify") as Gtk.Switch;
  208. bnotify.set_active(settings.get_boolean(key_newmail));
  209. bnotify.notify["active"].connect(() => {
  210. settings.set_boolean(key_newmail,bnotify.active);
  211. });
  212. var bautostart = builder.get_object("sAutostart") as Gtk.Switch;
  213. try { string content;
  214. if (GLib.FileUtils.get_contents(XDG_AUTORUN_FILE,out content)){
  215. if (GLib.Regex.match_simple(GNOME_AUTOSTART_DISABLED,content))
  216. bautostart.set_active(false);
  217. else bautostart.set_active(true);
  218. } else bautostart.set_active(false);
  219. } catch (FileError e) { stderr.printf("%s\n",e.message); }
  220. bautostart.notify["active"].connect(() => {
  221. if (bautostart.active) {
  222. string content;
  223. try {
  224. GLib.FileUtils.get_contents(smd_applet_desktop,out content);
  225. GLib.FileUtils.set_contents(XDG_AUTORUN_FILE,content);
  226. } catch (FileError e) { stderr.printf("%s\n",e.message); }
  227. } else {
  228. GLib.FileUtils.remove(XDG_AUTORUN_FILE);
  229. }
  230. });
  231. var bc = builder.get_object("bClose") as Gtk.Button;
  232. bc.clicked.connect(close_err_action);
  233. var bel = builder.get_object("bEditLoopCfg") as Gtk.Button;
  234. bel.clicked.connect((b) => {
  235. // if not existent, create the template first
  236. try {
  237. if (!is_smd_loop_configured()){
  238. GLib.Process.spawn_command_line_sync(
  239. "%s -t".printf(smd_loop_cmd));
  240. }
  241. string cmd = "gnome-open %s".printf(SMD_LOOP_CFG);
  242. GLib.Process.spawn_command_line_async(cmd);
  243. is_smd_loop_configured();
  244. } catch (GLib.SpawnError e) {
  245. stderr.printf("%s\n",e.message);
  246. }
  247. });
  248. var bepp = builder.get_object("bEditPushPullCfg") as Gtk.Button;
  249. bepp.clicked.connect((b) => {
  250. // if not existent, create the template first
  251. try {
  252. if (!is_smd_pushpull_configured()){
  253. GLib.Process.spawn_command_line_sync(
  254. "%s -t".printf(smd_push_cmd));
  255. }
  256. string cmd = "gnome-open %s".printf(SMD_PP_DEF_CFG);
  257. GLib.Process.spawn_command_line_async(cmd);
  258. is_smd_pushpull_configured();
  259. } catch (GLib.SpawnError e) {
  260. stderr.printf("%s\n",e.message);
  261. }
  262. });
  263. // menu popped up when the user clicks on the notification area
  264. sync_active = builder.get_object("sSyncActive") as Gtk.Switch;
  265. sync_active.notify["active"].connect(() => {
  266. if (sync_active.get_active()) unpause();
  267. else pause();
  268. });
  269. notebook = builder.get_object("nMain") as Gtk.Notebook;
  270. update_loglist();
  271. GLib.Timeout.add(2000,(() => {
  272. if (win.visible) {
  273. update_loglist(); update_logcontents();
  274. }
  275. return true; }));
  276. // status icon
  277. si = new Gtk.StatusIcon.from_icon_name("mail-send-receive");
  278. si.set_visible(true);
  279. si.set_tooltip_text("smd-applet is running");
  280. si.activate.connect((s) => {
  281. if ( error_mode )
  282. err_win.reshow_with_initial_size();
  283. else {
  284. win.show();
  285. if ( config_wait_mode ) notebook.page = 1;
  286. else notebook.page = 0;
  287. }
  288. });
  289. add_window(win);
  290. activate.connect(() => { });
  291. var quit = new SimpleAction("quit", null);
  292. var menu = builder.get_object("app-menu") as MenuModel;
  293. set_app_menu(menu);
  294. add_action(quit);
  295. quit.activate.connect(() => {
  296. thread_die = true;
  297. if ((int)pid != 0) {
  298. debug("sending SIGTERM to %d".printf(-(int)pid));
  299. Posix.kill((Posix.pid_t)(-(int)pid),Posix.SIGTERM);
  300. }
  301. this.quit();
  302. });
  303. // notification system
  304. unowned List<string> l = Notify.get_server_caps();
  305. notification_server_has_persistence = (0 <= l.index("persistence"));
  306. // error mode data
  307. command_hash = new GLib.HashTable<Gtk.Widget,string>(
  308. GLib.direct_hash,GLib.str_equal);
  309. }
  310. // ===================== smd-loop handling ============================
  311. // This thread fills the event queue, parsing the
  312. // stdout of a child process
  313. private void *smdThread() {
  314. bool rc = true;
  315. while(rc && !thread_die){
  316. debug("(re)starting smd-loop");
  317. try { rc = run_smd_loop(); }
  318. catch (Exit e) { rc = false; } // unrecoverable error
  319. }
  320. return null;
  321. }
  322. // force starts the thread even if there is no connection
  323. private void start_smdThread(bool force = false) {
  324. // if no network, we do not start the thread and enter pause mode
  325. // immediately
  326. if (!force && net_manager != null && !is_nm_connected(net_manager.state)) {
  327. sync_active.set_active(false);
  328. si.set_from_stock("gtk-media-pause");
  329. } else {
  330. // the thread fills the event queue
  331. thread = new GLib.Thread<void *>(null, smdThread);
  332. }
  333. }
  334. private bool eval_smd_loop_error_message(
  335. string args, string account, string host) throws GLib.RegexError{
  336. var context = new GLib.Regex("context\\(([^\\)]+)\\)");
  337. var cause = new GLib.Regex("probable-cause\\(([^\\)]+)\\)");
  338. var human = new GLib.Regex("human-intervention\\(([^\\)]+)\\)");
  339. var actions=new GLib.Regex("suggested-actions\\((.*)\\) *$");
  340. GLib.MatchInfo i_ctx=null, i_cause=null, i_human=null, i_act=null;
  341. if (! context.match(args,0,out i_ctx)){
  342. stderr.printf("smd-loop error with no context: %s\n",args);
  343. return true;
  344. }
  345. if (! cause.match(args,0,out i_cause)){
  346. stderr.printf("smd-loop error with no cause: %s\n",args);
  347. return true;
  348. }
  349. if (! human.match(args,0,out i_human)){
  350. stderr.printf("smd-loop error with no human: %s\n",args);
  351. return true;
  352. }
  353. var has_actions = actions.match(args,0,out i_act);
  354. if ( i_human.fetch(1) != "necessary" && i_cause.fetch(1) == "network"){
  355. events_lock.lock();
  356. events.insert(events.size, Event.network_error());
  357. events_lock.unlock();
  358. return true;
  359. }
  360. if ( i_human.fetch(1) != "necessary" ){
  361. stderr.printf("smd-loop giving an avoidable error: %s\n", args);
  362. events_lock.lock();
  363. events.insert(events.size, Event.generic_error(i_cause.fetch(1)));
  364. events_lock.unlock();
  365. return true;
  366. }
  367. string permissions = null;
  368. string mail_name = null;
  369. string mail_body = null;
  370. var commands = new Gee.ArrayList<string>(str_equal);
  371. if (has_actions) {
  372. string acts = i_act.fetch(1);
  373. var r_perm = new GLib.Regex("display-permissions\\(([^\\)]+)\\)");
  374. var r_mail = new GLib.Regex("display-mail\\(([^\\)]+)\\)");
  375. var r_cmd = new GLib.Regex("run\\(([^\\)]+)\\)");
  376. int from = 0;
  377. for (;acts != null && acts.length > 0;){
  378. MatchInfo i_cmd = null;
  379. if ( r_perm.match(acts,0,out i_cmd) ){
  380. i_cmd.fetch_pos(0,null,out from);
  381. string file = i_cmd.fetch(1);
  382. string output = null;
  383. string err = null;
  384. try {
  385. GLib.Process.spawn_command_line_sync(
  386. "ls -ld " + file, out output, out err);
  387. permissions = output + err;
  388. } catch (GLib.SpawnError e) {
  389. stderr.printf("Spawning ls: %s\n",e.message);
  390. }
  391. } else if ( r_mail.match(acts,0,out i_cmd) ){
  392. i_cmd.fetch_pos(0,null,out from);
  393. string file = i_cmd.fetch(1);
  394. string output = "";
  395. string err = null;
  396. try {
  397. mail_name = file;
  398. GLib.Process.spawn_command_line_sync(
  399. "cat " + file, out output, out err);
  400. mail_body = output + err;
  401. } catch (GLib.SpawnError e) {
  402. stderr.printf("Spawning ls: %s\n",e.message);
  403. }
  404. } else if ( r_cmd.match(acts,0,out i_cmd) ){
  405. string command = i_cmd.fetch(1);
  406. i_cmd.fetch_pos(0,null,out from);
  407. commands.insert(commands.size,command);
  408. } else {
  409. stderr.printf("Unrecognized action: %s\n",acts);
  410. break;
  411. }
  412. acts = acts.substring(from);
  413. }
  414. }
  415. events_lock.lock();
  416. events.insert(events.size, Event.error(
  417. account,host,i_ctx.fetch(1), i_cause.fetch(1),
  418. permissions, mail_name, mail_body, commands));
  419. events_lock.unlock();
  420. return false;
  421. }
  422. // return true if successful, false to stop due to an error
  423. private bool eval_smd_loop_message(string s){
  424. try {
  425. GLib.MatchInfo info = null;
  426. var r_tags = new GLib.Regex(
  427. "^([^:]+): smd-(client|server|loop|push|pull|pushpull)@([^:]+): TAGS:(.*)$");
  428. var r_skip = new GLib.Regex(
  429. "^([^:]+): smd-(client|server)@([^:]+): ERROR");
  430. if (r_skip.match(s,0,null)) { return true; }
  431. if (!r_tags.match(s,0,out info)) {
  432. debug("unhandled smd-loop message: %s".printf(s));
  433. return true;
  434. }
  435. var account = info.fetch(1);
  436. var host = info.fetch(3);
  437. var tags = info.fetch(4);
  438. GLib.MatchInfo i_args = null;
  439. var r_stats = new GLib.Regex(" stats::(.*)$");
  440. var r_error = new GLib.Regex(" error::(.*)$");
  441. if (r_stats.match(tags,0,out i_args)) {
  442. var r_neW = new GLib.Regex("new-mails\\(([0-9]+)\\)");
  443. var r_del = new GLib.Regex("del-mails\\(([0-9]+)\\)");
  444. GLib.MatchInfo i_new = null, i_del = null;
  445. var args = i_args.fetch(1);
  446. // check if matches
  447. var has_new = r_neW.match(args,0,out i_new);
  448. var has_del = r_del.match(args,0,out i_del);
  449. int new_mails = 0;
  450. if (has_new) { new_mails = int.parse(i_new.fetch(1)); }
  451. int del_mails = 0;
  452. if (has_del) { del_mails = int.parse(i_del.fetch(1)); }
  453. if (host == "localhost" && (new_mails > 0 || del_mails > 0)) {
  454. events_lock.lock();
  455. events.insert(events.size,
  456. Event.stats(account,host,new_mails, del_mails));
  457. events_lock.unlock();
  458. } else {
  459. // we skip non-error messages from the remote host
  460. }
  461. return true;
  462. } else if (r_error.match(tags,0,out i_args)) {
  463. var args = i_args.fetch(1);
  464. return eval_smd_loop_error_message(args,account,host);
  465. } else {
  466. stderr.printf("unhandled smd-loop message: %s\n",s);
  467. return true;
  468. }
  469. } catch (GLib.RegexError e) { stderr.printf("%s\n",e.message); }
  470. return true;
  471. }
  472. // runs smd loop once, returns true if it stopped
  473. // with a recoverable error and thus should be restarted
  474. private bool run_smd_loop() throws Exit {
  475. string[] cmd = { smd_loop_cmd, "-v" };
  476. int child_in;
  477. int child_out;
  478. int child_err;
  479. char[] buff = new char[10240];
  480. GLib.SpawnFlags flags = 0;
  481. bool rc;
  482. debug("spawning %s\n".printf(smd_loop_cmd));
  483. try {
  484. rc = GLib.Process.spawn_async_with_pipes(
  485. null,cmd,null,flags,() => {
  486. // create a new session
  487. Posix.setpgid(0,0);
  488. },
  489. out pid, out child_in, out child_out, out child_err);
  490. } catch (GLib.Error e) {
  491. stderr.printf("Unable to execute "+
  492. smd_loop_cmd+": "+e.message+"\n");
  493. throw new Exit.ABORT("Unable to run smd-loop");
  494. }
  495. if (rc) {
  496. var input = GLib.FileStream.fdopen(child_out,"r");
  497. string s = null;
  498. bool goon = true;
  499. while ( goon && (s = input.gets(buff)) != null && !thread_die) {
  500. debug("smd-loop outputs: %s".printf(s));
  501. goon = eval_smd_loop_message(s);
  502. debug("eval_smd_loop_message returned %d".printf((int)goon));
  503. }
  504. if ( s != null ) {
  505. // smd-loop prints the error tag as its last action
  506. if ( (s = input.gets(buff)) != null ){
  507. stderr.printf("smd-loop gave error tag but not died\n");
  508. stderr.printf("smd-loop has pid %d and prints %s\n",
  509. (int)pid, s);
  510. }
  511. }
  512. GLib.Process.close_pid(pid);
  513. Posix.kill((Posix.pid_t) (-(int)pid),Posix.SIGTERM);
  514. return goon; // maybe true, if s == null
  515. } else {
  516. stderr.printf("Unable to execute "+smd_loop_cmd+"\n");
  517. throw new Exit.ABORT("Unable to run smd-loop");
  518. }
  519. }
  520. // process an event in the events queue by notifying the user
  521. // with its message
  522. bool eat_event() {
  523. Event e = null;
  524. // in error mode no events are processed
  525. if ( error_mode ) return true;
  526. // fetch the event
  527. events_lock.lock();
  528. if ( events.size > 0) {
  529. e = events.first();
  530. events.remove_at(0);
  531. }
  532. events_lock.unlock();
  533. // notification
  534. if ( e != null && e.message != null) {
  535. bool notify_on_newail = false;
  536. notify_on_newail = settings.get_boolean(key_newmail);
  537. if (e.enter_network_error_mode && network_error_mode) {
  538. // we avoid notifying the network problem more than once
  539. } else if ((!e.is_error_event() && notify_on_newail) ||
  540. (e.is_error_event() && e.enter_network_error_mode)) {
  541. notification = new Notify.Notification(
  542. "Syncmaildir",e.message,e.message_icon);
  543. notification.set_hint("transient",new Variant.boolean(true));
  544. try { notification.show(); }
  545. catch (GLib.Error e) { stderr.printf("%s\n",e.message); }
  546. } else if (e.is_error_event()) {
  547. notification = new Notify.Notification(
  548. "Syncmaildir",e.message,e.message_icon);
  549. notification.set_timeout(0);
  550. notification.add_action("clicked","Handle error",
  551. (not, action) => { err_win.reshow_with_initial_size(); });
  552. try { notification.show(); }
  553. catch (GLib.Error e) { stderr.printf("%s\n",e.message); }
  554. }
  555. }
  556. // behavioural changes, like entering error mode
  557. if ( e != null && e.enter_error_mode ) {
  558. // {{{ error notification and widget setup
  559. si.set_from_icon_name("error");
  560. si.set_tooltip_text("smd-applet encountered an error");
  561. error_mode = true;
  562. if (!notification_server_has_persistence) si.set_visible(true);
  563. var l_ctx = builder.get_object("lContext") as Gtk.Label;
  564. var l_cause = builder.get_object("lCause") as Gtk.Label;
  565. l_ctx.set_text(e.context);
  566. l_cause.set_text(e.cause);
  567. command_hash.remove_all();
  568. var vb = builder.get_object("vbRun") as Gtk.Grid;
  569. foreach(Gtk.Widget w in vb.get_children()){ vb.remove(w); }
  570. if (e.permissions != null) {
  571. var l = builder.get_object("lPermissions") as Gtk.Label;
  572. l.set_text(e.permissions);
  573. }
  574. if (e.mail_name != null) {
  575. var fn = builder.get_object("eMailName") as Gtk.Entry;
  576. fn.set_text(e.mail_name);
  577. var l = builder.get_object("tvMail") as Gtk.TextView;
  578. Gtk.TextBuffer b = l.get_buffer();
  579. b.set_text(e.mail_body,-1);
  580. Gtk.TextIter it,subj;
  581. b.get_start_iter(out it);
  582. if (it.forward_search("Subject:",
  583. Gtk.TextSearchFlags.TEXT_ONLY, out subj,null,null)){
  584. var insert = b.get_insert();
  585. b.select_range(subj,subj);
  586. l.scroll_to_mark(insert,0.0,true,0.0,0.0);
  587. }
  588. }
  589. if (e.commands != null) {
  590. foreach (string command in e.commands) {
  591. var hb = new Gtk.Grid();
  592. hb.set_column_homogeneous(false);
  593. hb.set_column_spacing(10);
  594. string nice_command;
  595. try {
  596. GLib.MatchInfo i_mailto;
  597. var mailto_rex = new GLib.Regex("^gnome-open..mailto:");
  598. if ( mailto_rex.match(command,0,out i_mailto)) {
  599. nice_command =
  600. GLib.Uri.unescape_string(command).
  601. substring(12,70) + "...";
  602. } else {
  603. nice_command = command;
  604. }
  605. } catch (GLib.RegexError e) {
  606. nice_command = command;
  607. }
  608. var lbl = new Gtk.Label(nice_command);
  609. lbl.set_alignment(0.0f,0.5f);
  610. var but = new Gtk.Button.from_stock("gtk-execute");
  611. command_hash.insert(but,command);
  612. but.clicked.connect((b) => {
  613. int cmd_status;
  614. string output;
  615. string error;
  616. debug("executing: %s\n".printf(command_hash.lookup(b)));
  617. //XXX take host into account
  618. try{
  619. GLib.Process.spawn_command_line_sync(
  620. command_hash.lookup(b),
  621. out output,out error,out cmd_status);
  622. if (GLib.Process.if_exited(cmd_status) &&
  623. 0==GLib.Process.exit_status(cmd_status)){
  624. // OK!
  625. b.set_sensitive(false);
  626. } else {
  627. var w = new Gtk.MessageDialog(err_win,
  628. Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR,
  629. Gtk.ButtonsType.CLOSE,
  630. "An error occurred:\n%s\n%s",output,error);
  631. w.run();
  632. w.destroy();
  633. }
  634. } catch (GLib.SpawnError e) {
  635. stderr.printf("Spawning: %s\n",e.message);
  636. }
  637. });
  638. hb.attach(lbl,1,0,1,1);
  639. hb.attach(but,0,0,1,1);
  640. vb.add(hb);
  641. hb.show_all();
  642. }
  643. }
  644. var x = builder.get_object("fDisplayPermissions") as Gtk.Widget;
  645. x.visible = (e.permissions != null);
  646. x = builder.get_object("fDisplayMail") as Gtk.Widget;
  647. x.visible = (e.mail_name != null);
  648. x = builder.get_object("fRun") as Gtk.Widget;
  649. x.visible = (e.commands.size > 0);
  650. // }}}
  651. } else if (e != null && e.enter_network_error_mode ) {
  652. // network error warning
  653. network_error_mode = true;
  654. si.set_from_icon_name("dialog-warning");
  655. si.set_tooltip_text("Network error");
  656. } else if (e != null) {
  657. // no error
  658. network_error_mode = false;
  659. si.set_from_icon_name("mail-send-receive");
  660. si.set_tooltip_text("smd-applet is running");
  661. }
  662. return true; // re-schedule me please
  663. }
  664. // ===================== named signal handlers =======================
  665. // these are just wrappers for close_err
  666. private void close_err_action(Gtk.Button b){ reset_to_regular_run(); }
  667. private bool close_err_event(Gdk.EventAny e){
  668. reset_to_regular_run();
  669. return true;
  670. }
  671. private void reset_to_regular_run(bool force = false) {
  672. err_win.hide();
  673. error_mode = false;
  674. si.set_tooltip_text("smd-applet is running");
  675. si.set_from_icon_name("mail-send-receive");
  676. debug("joining smdThread");
  677. if (thread != null) thread.join();
  678. thread_die = false;
  679. debug("starting smdThread");
  680. start_smdThread(force);
  681. }
  682. // these are just wrappers for close_prefs
  683. private bool close_prefs_event(Gdk.EventAny e){
  684. close_prefs();
  685. return true;
  686. }
  687. // close the prefs button, eventually start the theread if exiting
  688. // config_wait_mode
  689. private void close_prefs(){
  690. win.hide();
  691. if (is_smd_stack_configured() && config_wait_mode) {
  692. config_wait_mode = false;
  693. // restore the default icon
  694. si.set_from_icon_name("mail-send-receive");
  695. // start the thread (if connected)
  696. debug("starting smdThread since smd stack is configured");
  697. start_smdThread();
  698. }
  699. }
  700. // pause/unpause the program
  701. private void pause() {
  702. debug("enter pause mode");
  703. if ((int)pid != 0) {
  704. debug("sending SIGTERM to %d".printf(-(int)pid));
  705. Posix.kill((Posix.pid_t)(-(int)pid),Posix.SIGTERM);
  706. }
  707. thread_die = true;
  708. si.set_from_stock("gtk-media-pause");
  709. si.set_tooltip_text("smd-applet is paused");
  710. }
  711. private void unpause() {
  712. debug("exit pause mode");
  713. reset_to_regular_run(true);
  714. }
  715. // ======================== config check ===========================
  716. private bool is_smd_loop_configured() {
  717. bool rc = GLib.FileUtils.test(SMD_LOOP_CFG,GLib.FileTest.EXISTS);
  718. var l = builder.get_object("bErrLoop") as Gtk.Box;
  719. if (!rc) l.show();
  720. else l.hide();
  721. return rc;
  722. }
  723. private bool is_smd_pushpull_configured() {
  724. bool rc = GLib.FileUtils.test(SMD_PP_DEF_CFG,GLib.FileTest.EXISTS);
  725. var l = builder.get_object("bErrPushPull") as Gtk.Box;
  726. if (!rc) l.show();
  727. else l.hide();
  728. return rc;
  729. }
  730. private bool is_smd_stack_configured() {
  731. var a = is_smd_loop_configured();
  732. var b = is_smd_pushpull_configured();
  733. return a && b;
  734. }
  735. // ======================== log window ================================
  736. private void update_loglist(){
  737. var tv = builder.get_object("tvLog") as Gtk.TextView;
  738. var b = tv.get_buffer();
  739. try {
  740. Dir d = GLib.Dir.open(SMD_LOGS_DIR);
  741. string file;
  742. var new_lognames = new Gee.ArrayList<string>(str_equal);
  743. while ( (file = d.read_name()) != null ){
  744. new_lognames.add(file);
  745. }
  746. if (!new_lognames.contains_all(lognames) ||
  747. !lognames.contains_all(new_lognames)) {
  748. ((Gtk.ListStore)cblogs.get_model()).clear();
  749. lognames.clear();
  750. foreach (string f in new_lognames) {
  751. lognames.add(f);
  752. cblogs.append_text(f);
  753. }
  754. if (lognames.size == 0) {
  755. b.set_text("No logs in %s".printf(SMD_LOGS_DIR),-1);
  756. } else {
  757. cblogs.set_title("Choose log file");
  758. cblogs.set_active(0);
  759. }
  760. }
  761. } catch (GLib.FileError e) {
  762. b.set_text("Unable to list directory %s".printf(SMD_LOGS_DIR),-1);
  763. }
  764. }
  765. void update_logcontents() {
  766. int selected = cblogs.get_active();
  767. if (selected >= 0) {
  768. string file = lognames.get(selected);
  769. string content;
  770. try {
  771. if (GLib.FileUtils.get_contents(
  772. SMD_LOGS_DIR+file,out content)){
  773. var tv = builder.get_object("tvLog") as Gtk.TextView;
  774. var b = tv.get_buffer();
  775. Gtk.TextIter end_iter, start_iter;
  776. b.get_end_iter(out end_iter);
  777. b.get_start_iter(out start_iter);
  778. if (content != b.get_text(start_iter,end_iter,false)) {
  779. b.set_text(content,-1);
  780. b.get_end_iter(out end_iter);
  781. tv.scroll_to_iter(end_iter, 0.0, true, 0.0, 0.0);
  782. }
  783. } else {
  784. stderr.printf("Unable to read %s\n",SMD_LOGS_DIR+file);
  785. }
  786. } catch (GLib.FileError e) {
  787. stderr.printf("Unable to read %s: %s\n",
  788. SMD_LOGS_DIR+file, e.message);
  789. }
  790. }
  791. }
  792. // ====================== public methods ==============================
  793. // starts the thread and the timeout handler
  794. public void start() throws Exit {
  795. // the timout function that will eventually notify the user
  796. GLib.Timeout.add(1000, eat_event);
  797. // before running, we need the whole smd stack
  798. // to be configured
  799. if (is_smd_stack_configured()) {
  800. start_smdThread();
  801. } else {
  802. config_wait_mode = true;
  803. }
  804. // windows will last for the whole execution,
  805. // so the (x) button should just hide them
  806. win.delete_event.connect(close_prefs_event);
  807. err_win.delete_event.connect(close_err_event);
  808. // we show the icon if we have to.
  809. // this is performed here and not in the constructor
  810. // since if we passed --configure the icon has not
  811. // to be shown
  812. if ( config_wait_mode ) {
  813. while ( Gtk.events_pending() ) Gtk.main_iteration();
  814. si.set_visible(false);
  815. // we wait a bit, hopefully the gnome bar will be drawn in the
  816. // meanwhile
  817. Posix.sleep(5);
  818. // we draw the icon
  819. si.set_visible(true);
  820. si.set_from_icon_name("error");
  821. // we process events to have the icon before the notification baloon
  822. while ( Gtk.events_pending() ) Gtk.main_iteration();
  823. // we do the notification
  824. notification = new Notify.Notification(
  825. "Syncmaildir","Syncmaildir is not configured properly.",
  826. "dialog-error");
  827. notification.set_hint("transient",new Variant.boolean(true));
  828. notification.add_action("configure","Configure now",((n,a) =>
  829. { si.activate(); }));
  830. try { notification.show(); }
  831. catch (GLib.Error e) { stderr.printf("%s\n",e.message); }
  832. }
  833. base.run();
  834. if (thread != null) thread.join();
  835. }
  836. } // class end
  837. // =================== main =====================================
  838. static int main(string[] args){
  839. string PREFIX = SMDConf.PREFIX;
  840. // handle prefix
  841. if (! GLib.FileUtils.test(PREFIX + SMD_APPLET_UI,GLib.FileTest.EXISTS)) {
  842. stderr.printf("error: file not found: %s + %s\n",
  843. PREFIX, SMD_APPLET_UI);
  844. smdApplet.smd_loop_cmd = "./smd-loop";
  845. stderr.printf("smd-applet not installed, " +
  846. "assuming smd-loop is: %s\n", smdApplet.smd_loop_cmd);
  847. smdApplet.smd_applet_ui = "./smd-applet.ui";
  848. stderr.printf("smd-applet not installed, " +
  849. "assuming smd-applet.ui is: %s\n", smdApplet.smd_applet_ui);
  850. smdApplet.smd_push_cmd = "./smd-push";
  851. stderr.printf("smd-applet not installed, " +
  852. "assuming smd-push is: %s\n", smdApplet.smd_push_cmd);
  853. smdApplet.smd_applet_desktop = "./smd-applet.desktop";
  854. stderr.printf("smd-applet not installed, " +
  855. "assuming smd-applet.desktop is: %s\n",
  856. smdApplet.smd_applet_desktop);
  857. } else {
  858. smdApplet.smd_loop_cmd = PREFIX + SMD_LOOP;
  859. smdApplet.smd_push_cmd = PREFIX + SMD_PUSH;
  860. smdApplet.smd_applet_ui = PREFIX + SMD_APPLET_UI;
  861. smdApplet.smd_applet_desktop = PREFIX + SMD_APPLET_DESKTOP;
  862. }
  863. var homedir = GLib.Environment.get_home_dir();
  864. SMD_LOGS_DIR = homedir+"/.smd/log/";
  865. SMD_LOOP_CFG = homedir+"/.smd/loop";
  866. SMD_PP_DEF_CFG = homedir+"/.smd/config.default";
  867. var conf_home = GLib.Environment.get_variable("XDG_CONFIG_HOME");
  868. if (conf_home != null)
  869. XDG_AUTORUN_FILE = conf_home+"/autostart/smd-applet.desktop";
  870. else
  871. XDG_AUTORUN_FILE = homedir + "/.config/autostart/smd-applet.desktop";
  872. GLib.DirUtils.create_with_parents(
  873. GLib.Path.get_dirname(XDG_AUTORUN_FILE),0700);
  874. // we init gtk+ and notify
  875. Gtk.init (ref args);
  876. Notify.init("smd-applet");
  877. GLib.OptionEntry[] oe = {
  878. GLib.OptionEntry () {
  879. long_name = "verbose", short_name = 'v',
  880. flags = 0, arg = GLib.OptionArg.NONE,
  881. arg_data = &verbose,
  882. description = "verbose output, for debugging only",
  883. arg_description = null },
  884. GLib.OptionEntry () {
  885. long_name = "smd-loop", short_name = 'l',
  886. flags = 0, arg = GLib.OptionArg.STRING,
  887. arg_data = &smdApplet.smd_loop_cmd,
  888. description = "override smd-loop command name, debugging only",
  889. arg_description = "program" },
  890. GLib.OptionEntry () { long_name = null }
  891. };
  892. var oc = new GLib.OptionContext(" - syncmaildir applet");
  893. oc.add_main_entries(oe,null);
  894. try { oc.parse(ref args); }
  895. catch (GLib.OptionError e) { stderr.printf("%s\n",e.message); return 1; }
  896. // go!
  897. try {
  898. var smd_applet = new smdApplet();
  899. smd_applet.start();
  900. } catch (Exit e) {
  901. stderr.printf("abort: %s\n",e.message);
  902. return 1;
  903. }
  904. return 0;
  905. }
  906. // vim:set ts=4 foldmethod=marker: