[tor-commits] [sandboxed-tor-browser/master] Bug 20778: Check updates in the background.

yawning at torproject.org yawning at torproject.org
Wed Dec 28 04:44:16 UTC 2016


commit 79ae41d7f63a825866ff67d581b87ad9bb7d615f
Author: Yawning Angel <yawning at schwanenlied.me>
Date:   Fri Dec 23 05:33:27 2016 +0000

    Bug 20778: Check updates in the background.
    
    Check for updates in the background and use a Desktop Notification (via
    libnotify) to prompt the user if they want to restart to apply the
    update.
    
    Additionally this sets the env var `TOR_SANDBOX` to `linux-v0` when
    launching firefox.
---
 ChangeLog                                          |   1 +
 README.md                                          |   2 +
 .../internal/sandbox/application.go                |   1 +
 .../internal/sandbox/hugbox.go                     |   4 +-
 .../internal/ui/config/config.go                   |  22 ++
 .../sandboxed-tor-browser/internal/ui/gtk/ui.go    | 170 ++++++++++-
 .../sandboxed-tor-browser/internal/ui/install.go   |  15 +-
 .../sandboxed-tor-browser/internal/ui/launch.go    |   2 +-
 .../internal/ui/notify/notify.go                   | 322 +++++++++++++++++++++
 src/cmd/sandboxed-tor-browser/internal/ui/ui.go    |  49 ++--
 .../sandboxed-tor-browser/internal/ui/update.go    |  61 ++--
 11 files changed, 589 insertions(+), 60 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 5344f14..dbde25a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,5 @@
 Changes in version 0.0.3 - UNRELEASED:
+ * Bug 20778: Check for updates in the background.
  * Bug 20851: If the incremental update fails, fall back to the complete
    update.
  * Bug 21055: Fall back gracefully if the Adwaita theme is not present.
diff --git a/README.md b/README.md
index 9e1ec83..43e199f 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ Runtime dependencies:
  * Gtk+ >= 3.14.0
  * (Optional) PulseAudio
  * (Optional) Adwaita Gtk+-2.0 theme
+ * (Optional) libnotify and a Desktop Notification daemon
 
 Build time dependencies:
 
@@ -29,6 +30,7 @@ Build time dependencies:
  * gb (https://getgb.io/ Yes I know it's behind fucking cloudflare)
  * Go (Tested with 1.7.x)
  * libseccomp2 >= 2.2.1
+ * libnotify
 
 Things that the sandbox breaks:
 
diff --git a/src/cmd/sandboxed-tor-browser/internal/sandbox/application.go b/src/cmd/sandboxed-tor-browser/internal/sandbox/application.go
index a126e4f..2d016bb 100644
--- a/src/cmd/sandboxed-tor-browser/internal/sandbox/application.go
+++ b/src/cmd/sandboxed-tor-browser/internal/sandbox/application.go
@@ -163,6 +163,7 @@ func RunTorBrowser(cfg *config.Config, manif *config.Manifest, tor *tor.Tor) (cm
 	h.setenv("TOR_CONTROL_PORT", "9151")
 	h.setenv("TOR_SKIP_LAUNCH", "1")
 	h.setenv("TOR_NO_DISPLAY_NETWORK_SETTINGS", "1")
+	h.setenv("TOR_SANDBOX", "linux-v0")
 
 	// Inject the AF_LOCAL compatibility hack stub into the filesystem, and
 	// supply the relevant args required for functionality.
diff --git a/src/cmd/sandboxed-tor-browser/internal/sandbox/hugbox.go b/src/cmd/sandboxed-tor-browser/internal/sandbox/hugbox.go
index bbc4333..260be34 100644
--- a/src/cmd/sandboxed-tor-browser/internal/sandbox/hugbox.go
+++ b/src/cmd/sandboxed-tor-browser/internal/sandbox/hugbox.go
@@ -341,8 +341,8 @@ func (h *hugbox) run() (*exec.Cmd, error) {
 		Debugf("sandbox: bwrap pid is: %v", cmd.Process.Pid)
 		Debugf("sandbox: child pid is: %v", info.Pid)
 
-		// This is more useful to us, since it's fork of bubblewrap that will
-		// execvp.
+		// This is more useful to us, since it's the bubblewrap child inside
+		// the container.
 		cmd.Process.Pid = info.Pid
 
 		doneCh <- nil
diff --git a/src/cmd/sandboxed-tor-browser/internal/ui/config/config.go b/src/cmd/sandboxed-tor-browser/internal/ui/config/config.go
index 9ffbd5c..bddc073 100644
--- a/src/cmd/sandboxed-tor-browser/internal/ui/config/config.go
+++ b/src/cmd/sandboxed-tor-browser/internal/ui/config/config.go
@@ -25,6 +25,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"time"
 
 	butils "git.schwanenlied.me/yawning/bulb.git/utils"
 	xdg "github.com/cep21/xdgbasedir"
@@ -299,6 +300,10 @@ type Config struct {
 	// Locale is the Tor Browser locale to install ("en-US", "ja").
 	Locale string `json:"locale,omitempty"`
 
+	// LastUpdateCheck is the UNIX time when the last update check was
+	// sucessfully completed.
+	LastUpdateCheck int64 `json:"lastUpdateCheck,omitEmpty"`
+
 	// ForceUpdate is set if the installed bundle is known to be obsolete.
 	ForceUpdate bool `json:"forceUpdate"`
 
@@ -375,6 +380,23 @@ func (cfg *Config) SetFirstLaunch(b bool) {
 	}
 }
 
+// NeedsUpdateCheck returns true if the bundle needs to be checked for updates,
+// and possibly updated.
+func (cfg *Config) NeedsUpdateCheck() bool {
+	const updateInterval = 60 * 60 * 2 // 2 hours, TBB behavior.
+	now := time.Now().Unix()
+	return (now > cfg.LastUpdateCheck+updateInterval) || cfg.LastUpdateCheck > now
+}
+
+// SetLastUpdateCheck sets the last update check time and marks the config
+// dirty.
+func (cfg *Config) SetLastUpdateCheck(t int64) {
+	if cfg.LastUpdateCheck != t {
+		cfg.LastUpdateCheck = t
+		cfg.isDirty = true
+	}
+}
+
 // SetForceUpdate sets the bundle as needed an update and marks the config
 // dirty.
 func (cfg *Config) SetForceUpdate(b bool) {
diff --git a/src/cmd/sandboxed-tor-browser/internal/ui/gtk/ui.go b/src/cmd/sandboxed-tor-browser/internal/ui/gtk/ui.go
index 4e526bb..0f85c76 100644
--- a/src/cmd/sandboxed-tor-browser/internal/ui/gtk/ui.go
+++ b/src/cmd/sandboxed-tor-browser/internal/ui/gtk/ui.go
@@ -18,17 +18,24 @@
 package gtk
 
 import (
+	"log"
 	"path/filepath"
 	"strings"
+	"time"
 
 	"github.com/gotk3/gotk3/gdk"
 	gtk3 "github.com/gotk3/gotk3/gtk"
 
 	"cmd/sandboxed-tor-browser/internal/data"
+	"cmd/sandboxed-tor-browser/internal/installer"
 	sbui "cmd/sandboxed-tor-browser/internal/ui"
 	"cmd/sandboxed-tor-browser/internal/ui/async"
+	"cmd/sandboxed-tor-browser/internal/ui/notify"
+	. "cmd/sandboxed-tor-browser/internal/utils"
 )
 
+const actionRestart = "restart"
+
 type gtkUI struct {
 	sbui.Common
 
@@ -39,9 +46,19 @@ type gtkUI struct {
 	installDialog  *installDialog
 	configDialog   *configDialog
 	progressDialog *progressDialog
+
+	updateNotification   *notify.Notification
+	updateNotificationCh chan string
 }
 
 func (ui *gtkUI) Run() error {
+	const (
+		updateMinInterval   = 30 * time.Second
+		updateCheckInterval = 2 * time.Hour
+		updateNagInterval   = 15 * time.Minute
+		gtkPumpInterval     = 1 * time.Second
+	)
+
 	if err := ui.Common.Run(); err != nil {
 		ui.bitch("Failed to run common UI: %v", err)
 		return err
@@ -49,6 +66,9 @@ func (ui *gtkUI) Run() error {
 	if ui.PrintVersion {
 		return nil
 	}
+	if ui.updateNotification == nil {
+		log.Printf("ui: libnotify wasn't found, no desktop notifications possible")
+	}
 
 	if ui.NeedsInstall() || ui.ForceInstall {
 		for {
@@ -80,7 +100,7 @@ func (ui *gtkUI) Run() error {
 				continue
 			}
 		}
-		ui.ForceConfig = true
+		ui.ForceConfig = true // Drop back to the config on failures.
 
 		// Launch
 		if err := ui.launch(); err != nil {
@@ -88,12 +108,121 @@ func (ui *gtkUI) Run() error {
 				ui.bitch("Failed to launch Tor Browser: %v", err)
 			}
 			continue
-		} else {
-			// Wait till the sandboxed process finishes.
-			ui.Cfg.SetFirstLaunch(false)
-			ui.Cfg.Sync()
-			return ui.Sandbox.Wait()
 		}
+
+		// Unset the first launch flag to skip the config on subsequent
+		// launches.
+		ui.Cfg.SetFirstLaunch(false)
+		ui.Cfg.Sync()
+
+		waitCh := make(chan error)
+		go func() {
+			waitCh <- ui.Sandbox.Wait()
+		}()
+
+		// Determine the time for the initial update check.
+		initialUpdateInterval := updateMinInterval
+		oldScheduledTime := time.Unix(ui.Cfg.LastUpdateCheck, 0).Add(updateCheckInterval)
+		Debugf("update: Previous scheduled update check: %v", oldScheduledTime)
+
+		if oldScheduledTime.After(time.Now()) {
+			deltaT := oldScheduledTime.Sub(time.Now())
+			if deltaT > updateMinInterval {
+				initialUpdateInterval = deltaT
+			}
+		}
+		Debugf("update: Initial scheduled update check: %v", initialUpdateInterval)
+
+		updateTimer := time.NewTimer(initialUpdateInterval)
+		defer updateTimer.Stop()
+
+		gtkPumpTicker := time.NewTicker(gtkPumpInterval)
+		defer gtkPumpTicker.Stop()
+
+		var update *installer.UpdateEntry
+	browserRunningLoop:
+		for {
+			select {
+			case err := <-waitCh:
+				return err
+			case <-gtkPumpTicker.C:
+				// This is so stupid, but is needed for notification actions
+				// to work.
+				gtk3.MainIteration()
+				continue
+			case action := <-ui.updateNotificationCh:
+				// Notification action was triggered, probably a restart.
+				log.Printf("update: Received notification action: %v", action)
+				if action == actionRestart {
+					break browserRunningLoop
+				}
+				continue
+			case <-updateTimer.C:
+			}
+
+			updateTimer.Stop()
+
+			// Only re-check for updates if we think we are up to date.
+			// Skipping re-fetching the metadata is fine, because we will
+			// do it as part of doUpdate() after the restart if it has
+			// aged too much.
+			if !ui.Cfg.ForceUpdate {
+				log.Printf("update: Starting scheduled update check.")
+
+				// Check for an update in the background.
+				async := async.NewAsync()
+				async.UpdateProgress = func(s string) {}
+
+				go func() {
+					update = ui.CheckUpdate(async)
+					async.Done <- true
+				}()
+
+				/// Wait for the check to complete.
+				select {
+				case err := <-waitCh: // User exited browser while checking.
+					return err
+				case <-async.Done:
+				}
+
+				if async.Err != nil {
+					log.Printf("update: Failed background update check: %v", async.Err)
+				}
+
+				if update != nil {
+					log.Printf("update: An update is available: %v", update.DisplayVersion)
+				} else {
+					log.Printf("update: The bundle is up to date")
+				}
+			}
+
+			if ui.Cfg.ForceUpdate {
+				log.Printf("update: Displaying notification.")
+				ui.notifyUpdate(update)
+				updateTimer.Reset(updateNagInterval)
+			} else {
+				updateTimer.Reset(updateCheckInterval)
+			}
+		}
+
+		// If we are here, the user wants to restart to apply an update.
+		gtkPumpTicker.Stop()
+
+		if ui.updateNotification != nil {
+			ui.updateNotification.Close()
+		}
+
+		// Kill the browser.  It's not as if firefox does the right thing on
+		// SIGTERM/SIGINT and we have the pid of the bubblewrap child instead
+		// of the firefox process anyway...
+		//
+		// https://bugzilla.mozilla.org/show_bug.cgi?id=336193
+		ui.Sandbox.Process.Kill()
+		<-waitCh
+
+		ui.PendingUpdate = update
+		ui.ForceConfig = false
+		ui.NoKillTor = true // Don't re-lauch tor on the first pass.
 	}
 }
 
@@ -101,6 +230,12 @@ func (ui *gtkUI) Term() {
 	// By the time this is run, we have exited the Gtk+ event loop, so we
 	// can assume we have exclusive ownership of the UI state.
 	ui.Common.Term()
+
+	if ui.updateNotification != nil {
+		ui.updateNotification.Close()
+		ui.updateNotification = nil
+		notify.Uninit()
+	}
 }
 
 func Init() (sbui.UI, error) {
@@ -150,6 +285,16 @@ func Init() (sbui.UI, error) {
 		}
 	}
 
+	// Initialize the Desktop Notification interface.
+	if err = notify.Init("Sandboxed Tor Browser"); err == nil {
+		ui.updateNotification = notify.New("", "", ui.iconPixbuf)
+		ui.updateNotification.SetTimeout(15 * 1000)
+		ui.updateNotification.AddAction(actionRestart, "Restart Now")
+		ui.updateNotificationCh = ui.updateNotification.ActionChan()
+	} else {
+		ui.updateNotificationCh = make(chan string)
+	}
+
 	return ui, nil
 }
 
@@ -159,7 +304,7 @@ func (ui *gtkUI) onDestroy() {
 
 func (ui *gtkUI) launch() error {
 	// If we don't need to update, and would just launch, quash the UI.
-	checkUpdate := ui.Cfg.ForceUpdate
+	checkUpdate := ui.Cfg.ForceUpdate || ui.Cfg.NeedsUpdateCheck()
 	squelchUI := !checkUpdate && ui.Cfg.UseSystemTor
 
 	async := async.NewAsync()
@@ -184,6 +329,17 @@ func (ui *gtkUI) bitch(format string, a ...interface{}) {
 	ui.forceRedraw()
 }
 
+func (ui *gtkUI) notifyUpdate(update *installer.UpdateEntry) {
+	if update == nil {
+		panic("ui: notifyUpdate called with no update metadata")
+	}
+
+	if ui.updateNotification != nil {
+		ui.updateNotification.Update("A Tor Browser update is available.", "Please restart to update to version "+update.DisplayVersion+".", ui.iconPixbuf)
+		ui.updateNotification.Show()
+	}
+}
+
 func (ui *gtkUI) pixbufFromAsset(asset string) (*gdk.Pixbuf, error) {
 	d, err := data.Asset(asset)
 	if err != nil {
diff --git a/src/cmd/sandboxed-tor-browser/internal/ui/install.go b/src/cmd/sandboxed-tor-browser/internal/ui/install.go
index ac9f246..4ae4256 100644
--- a/src/cmd/sandboxed-tor-browser/internal/ui/install.go
+++ b/src/cmd/sandboxed-tor-browser/internal/ui/install.go
@@ -20,12 +20,15 @@ import (
 	"fmt"
 	"io/ioutil"
 	"log"
+	"net"
 	"os"
 	"path/filepath"
 	"runtime"
+	"time"
 
 	"cmd/sandboxed-tor-browser/internal/data"
 	"cmd/sandboxed-tor-browser/internal/installer"
+	"cmd/sandboxed-tor-browser/internal/tor"
 	. "cmd/sandboxed-tor-browser/internal/ui/async"
 	"cmd/sandboxed-tor-browser/internal/ui/config"
 	"cmd/sandboxed-tor-browser/internal/utils"
@@ -59,8 +62,14 @@ func (c *Common) DoInstall(async *Async) {
 	}
 
 	// Get the Dial() routine used to reach the external network.
-	dialFn, err := c.launchTor(async, true)
-	if err != nil {
+	var dialFn dialFunc
+	if err := c.launchTor(async, true); err != nil {
+		async.Err = err
+		return
+	}
+	if dialFn, err = c.getTorDialFunc(); err == tor.ErrTorNotRunning {
+		dialFn = net.Dial
+	} else if err != nil {
 		async.Err = err
 		return
 	}
@@ -85,6 +94,7 @@ func (c *Common) DoInstall(async *Async) {
 			return
 		}
 	}
+	checkAt := time.Now().Unix()
 
 	log.Printf("install: Version: %v Downloads: %v", version, downloads)
 
@@ -143,6 +153,7 @@ func (c *Common) DoInstall(async *Async) {
 	}
 
 	// Set the appropriate bits in the config.
+	c.Cfg.SetLastUpdateCheck(checkAt)
 	c.Cfg.SetForceUpdate(false)
 	c.Cfg.SetFirstLaunch(true)
 
diff --git a/src/cmd/sandboxed-tor-browser/internal/ui/launch.go b/src/cmd/sandboxed-tor-browser/internal/ui/launch.go
index 49b7663..e929fa7 100644
--- a/src/cmd/sandboxed-tor-browser/internal/ui/launch.go
+++ b/src/cmd/sandboxed-tor-browser/internal/ui/launch.go
@@ -63,7 +63,7 @@ func (c *Common) DoLaunch(async *Async, checkUpdates bool) {
 	// Start tor if required.
 	log.Printf("launch: Connecting to the Tor network.")
 	async.UpdateProgress("Connecting to the Tor network.")
-	if _, async.Err = c.launchTor(async, false); async.Err != nil {
+	if async.Err = c.launchTor(async, false); async.Err != nil {
 		return
 	}
 
diff --git a/src/cmd/sandboxed-tor-browser/internal/ui/notify/notify.go b/src/cmd/sandboxed-tor-browser/internal/ui/notify/notify.go
new file mode 100644
index 0000000..50f156c
--- /dev/null
+++ b/src/cmd/sandboxed-tor-browser/internal/ui/notify/notify.go
@@ -0,0 +1,322 @@
+// notify.go - Desktop Notification interface.
+// Copyright (C) 2016  Yawning Angel.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+// Package notify interfaces with the Destop Notification daemon, as defined
+// by the desktop notifications spec, via the libnotify library.
+//
+// Note: Instead of linking libnotify, the library is opportunistically loaded
+// at runtime via dlopen().  This is not applied to glib/gdk as those are
+// pulled in by virtue of the application being a Gtk app.
+package notify
+
+// #cgo pkg-config: glib-2.0 gdk-3.0
+// #cgo LDFLAGS: -ldl
+//
+// #include <libnotify/notify.h>
+// #include <dlfcn.h>
+// #include <stdio.h>
+// #include <stdlib.h>
+// #include <string.h>
+// #include <assert.h>
+//
+// extern void actionCallbackHandler(void *, char *);
+//
+// static int initialized = 0;
+// static int supports_actions = 0;
+//
+// static gboolean (*init_fn)(const char *) = NULL;
+// static void (*uninit_fn)(void) = NULL;
+// static GList *(*get_server_caps_fn)(void) = NULL;
+//
+// static NotifyNotification *(*new_fn)(const char *, const char *, const char *) = NULL;
+// static void (*update_fn) (NotifyNotification *, const char *, const char *, const char *) = NULL;
+// static gboolean (*show_fn)(NotifyNotification *, GError **) = NULL;
+// static void (*set_timeout_fn)(NotifyNotification *, gint timeout) = NULL;
+// static void (*set_image_fn)(NotifyNotification *, GdkPixbuf *) = NULL;
+// static void (*add_action_fn)(NotifyNotification *, const char *, const char *, NotifyActionCallback, gpointer, GFreeFunc) = NULL;
+// static void (*close_fn)(NotifyNotification *, GError **) = NULL;
+//
+// static void
+// notify_action_cb(NotifyNotification *notification, char *action, gpointer user_data) {
+//   actionCallbackHandler(user_data, action);
+// }
+//
+// static int
+// init_libnotify(const char *app_name) {
+//    void *handle = NULL;
+//    GList *caps;
+//
+//    if (initialized != 0) {
+//      return initialized;
+//    }
+//    initialized = -1;
+//
+//    handle = dlopen("libnotify.so.4", RTLD_LAZY);
+//    if (handle == NULL) {
+//      fprintf(stderr, "ui: Failed to dlopen() 'libnotify.so.4': %s\n", dlerror());
+//      goto out;
+//    }
+//
+//    // Load all the symbols that we need.
+//    if ((init_fn = dlsym(handle, "notify_init")) == NULL) {
+//      fprintf(stderr, "ui: Failed to find 'notify_init()': %s\n", dlerror());
+//      goto out;
+//    }
+//    if ((uninit_fn = dlsym(handle, "notify_uninit")) == NULL) {
+//      fprintf(stderr, "ui: Failed to find 'notify_uninit()': %s\n", dlerror());
+//      goto out;
+//    }
+//    if ((get_server_caps_fn = dlsym(handle, "notify_get_server_caps")) == NULL) {
+//      fprintf(stderr, "ui: Failed to find 'notify_get_server_caps()': %s\n", dlerror());
+//      goto out;
+//    }
+//    if ((new_fn = dlsym(handle, "notify_notification_new")) == NULL) {
+//      fprintf(stderr, "ui: Failed to find 'notify_notification_new()': %s\n", dlerror());
+//      goto out;
+//    }
+//    if ((update_fn = dlsym(handle, "notify_notification_update")) == NULL) {
+//      fprintf(stderr, "ui: Failed to find 'notify_notification_update()': %s\n", dlerror());
+//      goto out;
+//    }
+//    if ((show_fn = dlsym(handle, "notify_notification_show")) == NULL) {
+//      fprintf(stderr, "ui: Failed to find 'notify_notification_show()': %s\n", dlerror());
+//      goto out;
+//    }
+//    if ((set_timeout_fn = dlsym(handle, "notify_notification_set_timeout")) == NULL) {
+//      fprintf(stderr, "ui: Failed to find 'notify_notification_set_timeout()': %s\n", dlerror());
+//      goto out;
+//    }
+//    if ((set_image_fn = dlsym(handle, "notify_notification_set_image_from_pixbuf")) == NULL) {
+//      fprintf(stderr, "ui: Failed to find'notify_notification_set_image_from_pixbuf': %s\n", dlerror());
+//      goto out;
+//    }
+//    if ((add_action_fn = dlsym(handle, "notify_notification_add_action")) ==  NULL) {
+//      fprintf(stderr, "ui: Failed to find'notify_notification_add_action': %s\n", dlerror());
+//      goto out;
+//    }
+//    if ((close_fn = dlsym(handle, "notify_notification_close")) == NULL) {
+//      fprintf(stderr, "ui: Failed to find'notify_notification_close': %s\n", dlerror());
+//      goto out;
+//    }
+//
+//    // Initialize libnotify.
+//    if (init_fn(app_name) == TRUE) {
+//      initialized = 0;
+//    }
+//
+//    // Figure out if we are talking to the stupid fucking Ubuntu notification
+//    // daemon, which doesn't support actions.
+//    caps = get_server_caps_fn();
+//    if (caps != NULL) {
+//      GList *c;
+//      for (c = caps; c != NULL; c = c->next) {
+//         if (strcmp((char*)c->data, "actions") == 0) {
+//           supports_actions = 1;
+//         }
+//      }
+//      g_list_foreach(caps, (GFunc)g_free, NULL);
+//      g_list_free(caps);
+//    }
+//
+// out:
+//    if (initialized != 0 && handle != NULL) {
+//      dlclose(handle);
+//   }
+//    return initialized;
+// }
+//
+// static void
+// uninit_libnotify(void) {
+//   if (initialized != 0) {
+//     return;
+//   }
+//   initialized = -1;
+//   uninit_fn();
+// }
+//
+// static NotifyNotification *
+// n_new(const char *summary, const char *body) {
+//   if (initialized != 0) {
+//     return NULL;
+//   }
+//   return new_fn(summary, body, NULL);
+// }
+//
+// static void
+// n_update(NotifyNotification *n, const char *summary, const char *body) {
+//   assert(n != NULL);
+//   update_fn(n, summary, body, NULL);
+// }
+//
+// static void
+// n_show(NotifyNotification *n) {
+//   assert(n != NULL);
+//   show_fn(n, NULL);
+// }
+//
+// static void
+// n_set_timeout(NotifyNotification *n, int timeout) {
+//   assert(n != NULL);
+//   set_timeout_fn(n, timeout);
+// }
+//
+// static void
+// n_set_image(NotifyNotification *n, void *pixbuf) {
+//   assert(n != NULL);
+//   set_image_fn(n, GDK_PIXBUF(pixbuf));
+// }
+//
+// static void
+// n_add_action(NotifyNotification *n, const char *action, const char *label, void *user_data) {
+//   assert(n != NULL);
+//   if (supports_actions) {
+//     add_action_fn(n, action, label, NOTIFY_ACTION_CALLBACK(notify_action_cb), user_data, NULL);
+//   }
+// }
+//
+// static void
+// n_close(NotifyNotification *n) {
+//   assert(n != NULL);
+//   close_fn(n, NULL);
+// }
+import "C"
+
+import (
+	"errors"
+	"runtime"
+	"unsafe"
+
+	"github.com/gotk3/gotk3/gdk"
+)
+
+const (
+	// EXPIRES_DEFAULT is the default expiration timeout.
+	EXPIRES_DEFAULT = C.NOTIFY_EXPIRES_DEFAULT
+
+	// EXPIRES_NEVER is the infinite expiration timeout.
+	EXPIRES_NEVER = C.NOTIFY_EXPIRES_NEVER
+)
+
+var callbackChans map[unsafe.Pointer]chan string
+
+// Notification is a `NotifyNotification` instance.
+type Notification struct {
+	n *C.NotifyNotification
+}
+
+// ActionChan returns the channel that actions will be written to.
+func (n *Notification) ActionChan() chan string {
+	return callbackChans[unsafe.Pointer(n.n)]
+}
+
+// Update updates the notification.  Like the libnotify counterpart, Show()
+// must be called to refresh the notification.
+func (n *Notification) Update(summary, body string, icon *gdk.Pixbuf) {
+	cSummary := C.CString(summary)
+	defer C.free(unsafe.Pointer(cSummary))
+	cBody := C.CString(body)
+	defer C.free(unsafe.Pointer(cBody))
+
+	C.n_update(n.n, cSummary, cBody)
+	n.SetImage(icon)
+}
+
+// Show (re-)displays the notification.
+func (n *Notification) Show() {
+	C.n_show(n.n)
+}
+
+// SetTimeout sets the notification timeout to the value specified in
+// milliseconds.
+func (n *Notification) SetTimeout(timeout int) {
+	C.n_set_timeout(n.n, C.int(timeout))
+}
+
+// SetImage sets the notification image to the specified GdkPixbuf.
+func (n *Notification) SetImage(pixbuf *gdk.Pixbuf) {
+	C.n_set_image(n.n, unsafe.Pointer(pixbuf.GObject))
+}
+
+// AddAction adds an action to the notification.
+func (n *Notification) AddAction(action, label string) {
+	cAction := C.CString(action)
+	defer C.free(unsafe.Pointer(cAction))
+	cLabel := C.CString(label)
+	defer C.free(unsafe.Pointer(cLabel))
+
+	C.n_add_action(n.n, cAction, cLabel, unsafe.Pointer(n))
+}
+
+// Close hides the specified nitification.
+func (n *Notification) Close() {
+	C.n_close(n.n)
+}
+
+// ErrNotSupported is the error returned when libnotify is missing or has
+// failed to initialize.
+var ErrNotSupported = errors.New("libnotify not installed or service not running")
+
+// Init initializes the Desktop Notification interface.
+func Init(appName string) error {
+	cstr := C.CString(appName)
+	defer C.free(unsafe.Pointer(cstr))
+	if C.init_libnotify(cstr) != 0 {
+		return ErrNotSupported
+	}
+	return nil
+}
+
+// Uninit cleans up the Desktop Notification interface, prior to termination.
+func Uninit() {
+	C.uninit_libnotify()
+}
+
+// New returns a new Notification.
+func New(summary, body string, icon *gdk.Pixbuf) *Notification {
+	cSummary := C.CString(summary)
+	defer C.free(unsafe.Pointer(cSummary))
+	cBody := C.CString(body)
+	defer C.free(unsafe.Pointer(cBody))
+
+	n := new(Notification)
+	n.n = C.n_new(cSummary, cBody)
+	if n.n == nil {
+		panic("libnotify: notify_notification_new() returned NULL")
+	}
+	callbackChans[unsafe.Pointer(n.n)] = make(chan string)
+
+	runtime.SetFinalizer(n, func(n *Notification) {
+		delete(callbackChans, unsafe.Pointer(n.n))
+		C.g_object_unref(n.n)
+	})
+	n.SetImage(icon)
+
+	return n
+}
+
+//export actionCallbackHandler
+func actionCallbackHandler(nPtr unsafe.Pointer, actionPtr *C.char) {
+	action := C.GoString(actionPtr)
+	n := (*Notification)(nPtr)
+	ch := n.ActionChan()
+	go func() {
+		ch <- action
+	}()
+}
+
+func init() {
+	callbackChans = make(map[unsafe.Pointer]chan string)
+}
diff --git a/src/cmd/sandboxed-tor-browser/internal/ui/ui.go b/src/cmd/sandboxed-tor-browser/internal/ui/ui.go
index ba0f293..d30d74c 100644
--- a/src/cmd/sandboxed-tor-browser/internal/ui/ui.go
+++ b/src/cmd/sandboxed-tor-browser/internal/ui/ui.go
@@ -104,8 +104,11 @@ type Common struct {
 	logPath  string
 	logFile  *os.File
 
+	PendingUpdate *installer.UpdateEntry
+
 	ForceInstall   bool
 	ForceConfig    bool
+	NoKillTor      bool
 	AdvancedConfig bool
 	PrintVersion   bool
 }
@@ -272,7 +275,19 @@ func (c *Common) NeedsInstall() bool {
 
 type dialFunc func(string, string) (net.Conn, error)
 
-func (c *Common) launchTor(async *Async, onlySystem bool) (dialFunc, error) {
+func (c *Common) getTorDialFunc() (dialFunc, error) {
+	if c.tor == nil {
+		return nil, tor.ErrTorNotRunning
+	}
+
+	dialer, err := c.tor.Dialer()
+	if err != nil {
+		return nil, err
+	}
+	return dialer.Dial, nil
+}
+
+func (c *Common) launchTor(async *Async, onlySystem bool) error {
 	var err error
 	defer func() {
 		if async.Err != nil && c.tor != nil {
@@ -281,23 +296,27 @@ func (c *Common) launchTor(async *Async, onlySystem bool) (dialFunc, error) {
 		}
 	}()
 
-	if c.tor != nil {
+	if c.tor != nil && !c.NoKillTor {
 		log.Printf("launch: Shutting down old tor.")
 		c.tor.Shutdown()
 		c.tor = nil
 	}
 
-	if c.Cfg.UseSystemTor {
+	if c.tor != nil && c.NoKillTor {
+		// Only the first re-launch should be skipped.
+		log.Printf("launch: Reusing old tor.")
+		c.NoKillTor = false
+	} else if c.Cfg.UseSystemTor {
 		if c.tor, err = tor.NewSystemTor(c.Cfg); err != nil {
 			async.Err = err
-			return nil, err
+			return err
 		}
 	} else if !onlySystem {
 		// Build the torrc.
 		torrc, err := tor.CfgToSandboxTorrc(c.Cfg, Bridges)
 		if err != nil {
 			async.Err = err
-			return nil, err
+			return err
 		}
 
 		os.Remove(filepath.Join(c.Cfg.TorDataDir, "control_port"))
@@ -306,36 +325,28 @@ func (c *Common) launchTor(async *Async, onlySystem bool) (dialFunc, error) {
 		cmd, err := sandbox.RunTor(c.Cfg, c.Manif, torrc)
 		if err != nil {
 			async.Err = err
-			return nil, err
+			return err
 		}
 
 		async.UpdateProgress("Waiting on Tor bootstrap.")
 		c.tor = tor.NewSandboxedTor(c.Cfg, cmd)
 		if err = c.tor.DoBootstrap(c.Cfg, async); err != nil {
 			async.Err = err
-			return nil, err
+			return err
 		}
 	} else if !(c.NeedsInstall() || c.ForceInstall) {
 		// That's odd, we only asked for a system tor, but we should be capable
 		// of launching tor ourselves.  Don't use a direct connection.
 		err = fmt.Errorf("tor bootstrap would be skipped, when we could launch")
 		async.Err = err
-		return nil, err
+		return err
 	}
 
-	// If we managed to launch tor...
-	if c.tor != nil {
-		// Query the socks port, setup the dialer.
-		if dialer, err := c.tor.Dialer(); err != nil {
-			async.Err = err
-			return nil, err
-		} else {
-			return dialer.Dial, nil
-		}
+	if c.tor != nil || onlySystem {
+		return nil
 	}
 
-	// We must be installing, without a tor daemon already running.
-	return net.Dial, nil
+	return tor.ErrTorNotRunning
 }
 
 type lockFile struct {
diff --git a/src/cmd/sandboxed-tor-browser/internal/ui/update.go b/src/cmd/sandboxed-tor-browser/internal/ui/update.go
index 1994e64..da69562 100644
--- a/src/cmd/sandboxed-tor-browser/internal/ui/update.go
+++ b/src/cmd/sandboxed-tor-browser/internal/ui/update.go
@@ -22,6 +22,7 @@ import (
 	"encoding/hex"
 	"fmt"
 	"log"
+	"time"
 
 	"cmd/sandboxed-tor-browser/internal/installer"
 	"cmd/sandboxed-tor-browser/internal/sandbox"
@@ -41,13 +42,13 @@ func (c *Common) CheckUpdate(async *Async) *installer.UpdateEntry {
 		async.Err = tor.ErrTorNotRunning
 		return nil
 	}
-	dialer, err := c.tor.Dialer()
+	dialFn, err := c.getTorDialFunc()
 	if err != nil {
 		async.Err = err
 		return nil
 	}
 
-	client := newHPKPGrabClient(dialer.Dial)
+	client := newHPKPGrabClient(dialFn)
 
 	// Determine where the update metadata should be fetched from.
 	updateURLs := []string{}
@@ -90,15 +91,21 @@ func (c *Common) CheckUpdate(async *Async) *installer.UpdateEntry {
 		async.Err = fmt.Errorf("failed to download update metadata")
 		return nil
 	}
+	checkAt := time.Now().Unix()
 
 	// If there is an update, tag the installed bundle as stale...
 	if update == nil {
 		log.Printf("update: Installed bundle is current.")
 		c.Cfg.SetForceUpdate(false)
+	} else if !c.Manif.BundleUpdateVersionValid(update.AppVersion) {
+		log.Printf("update: Update server provided a downgrade: '%v'", update.AppVersion)
+		async.Err = fmt.Errorf("update server provided a downgrade: '%v'", update.AppVersion)
+		return nil
 	} else {
 		log.Printf("update: Installed bundle needs updating.")
 		c.Cfg.SetForceUpdate(true)
 	}
+	c.Cfg.SetLastUpdateCheck(checkAt)
 
 	// ... and flush the config.
 	if async.Err = c.Cfg.Sync(); async.Err != nil {
@@ -112,22 +119,17 @@ func (c *Common) CheckUpdate(async *Async) *installer.UpdateEntry {
 // validates it with the hash in the patch datastructure, and the known MAR
 // signing keys.
 func (c *Common) FetchUpdate(async *Async, patch *installer.Patch) []byte {
-	var dialFn dialFunc
-
 	// Launch the tor daemon if needed.
 	if c.tor == nil {
-		dialFn, async.Err = c.launchTor(async, false)
+		async.Err = c.launchTor(async, false)
 		if async.Err != nil {
 			return nil
 		}
-	} else {
-		// Otherwise, retreive the dialer.
-		dialer, err := c.tor.Dialer()
-		if err != nil {
-			async.Err = err
-			return nil
-		}
-		dialFn = dialer.Dial
+	}
+	dialFn, err := c.getTorDialFunc()
+	if err != nil {
+		async.Err = err
+		return nil
 	}
 
 	// Download the MAR file.
@@ -178,20 +180,20 @@ func (c *Common) doUpdate(async *Async) {
 		patchComplete = "complete"
 	)
 
-	// Check for updates.
-	update := c.CheckUpdate(async)
-	if async.Err != nil || update == nil {
-		// Something either broke, or the bundle is up to date.  The caller
-		// needs to check async.Err, and either way there's nothing more that
-		// can be done.
-		return
-	}
-
-	// Ensure that the update entry version is actually neweer.
-	if !c.Manif.BundleUpdateVersionValid(update.AppVersion) {
-		log.Printf("update: Update server provided a downgrade: '%v'", update.AppVersion)
-		async.Err = fmt.Errorf("update server provided a downgrade: '%v'", update.AppVersion)
-		return
+	// Check for updates, unless we have sufficiently fresh metatdata already.
+	var update *installer.UpdateEntry
+	if c.PendingUpdate != nil && !c.Cfg.NeedsUpdateCheck() {
+		update = c.PendingUpdate
+		c.PendingUpdate = nil
+	} else {
+		update = c.CheckUpdate(async)
+		if async.Err != nil || update == nil {
+			// Something either broke, or the bundle is up to date.  The caller
+			// needs to check async.Err, and either way there's nothing more that
+			// can be done.
+			return
+		}
+		c.PendingUpdate = nil
 	}
 
 	// Figure out the best MAR to download.
@@ -258,7 +260,7 @@ func (c *Common) doUpdate(async *Async) {
 			continue
 		}
 
-		// Failues past this point are catastrophic in that, the on-disk
+		// Failures past this point are catastrophic in that, the on-disk
 		// bundle is up to date, but the post-update tasks have failed.
 
 		// Reinstall the autoconfig stuff.
@@ -283,8 +285,9 @@ func (c *Common) doUpdate(async *Async) {
 		if !c.Cfg.UseSystemTor {
 			log.Printf("launch: Reconnecting to the Tor network.")
 			async.UpdateProgress("Reconnecting to the Tor network.")
-			_, async.Err = c.launchTor(async, false)
+			async.Err = c.launchTor(async, false)
 		}
+
 		return
 	}
 



More information about the tor-commits mailing list