commit 79ae41d7f63a825866ff67d581b87ad9bb7d615f Author: Yawning Angel yawning@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 }
tor-commits@lists.torproject.org