osx: Implement auto-upgrade function using Sparkle

During the upgrade, NFD and ndn-autoupdate will be stopped (and, if
enabled, automatically started with new versions).

Change-Id: I010ae4a8aa6f99a0039d58065ab7eb0c9189a979
diff --git a/.waf-tools/osx-frameworks.py b/.waf-tools/osx-frameworks.py
new file mode 100644
index 0000000..254fa8a
--- /dev/null
+++ b/.waf-tools/osx-frameworks.py
@@ -0,0 +1,69 @@
+#! /usr/bin/env python
+# encoding: utf-8
+
+from waflib import Logs, Utils
+from waflib.Configure import conf
+
+OSX_SECURITY_CODE='''
+#import <AppKit/AppKit.h>
+#import <Foundation/Foundation.h>
+#import <Sparkle/Sparkle.h>
+#import <CoreWLAN/CWInterface.h>
+#import <CoreWLAN/CoreWLAN.h>
+#import <CoreWLAN/CoreWLANConstants.h>
+#import <CoreWLAN/CoreWLANTypes.h>
+int main() { }
+'''
+
+@conf
+def configure(conf):
+    if Utils.unversioned_sys_platform () == "darwin":
+        conf.check_cxx(framework_name='Foundation', uselib_store='OSX_FOUNDATION', mandatory=False, compile_filename='test.mm')
+        conf.check_cxx(framework_name='AppKit',     uselib_store='OSX_APPKIT',     mandatory=False, compile_filename='test.mm')
+        conf.check_cxx(framework_name='CoreWLAN',   uselib_store='OSX_COREWLAN',   define_name='HAVE_COREWLAN',
+                       use="OSX_FOUNDATION", mandatory=False, compile_filename='test.mm')
+
+        def check_sparkle(**kwargs):
+          conf.check_cxx(framework_name="Sparkle", header_name=["Foundation/Foundation.h", "AppKit/AppKit.h"],
+                         uselib_store='OSX_SPARKLE', define_name='HAVE_SPARKLE', mandatory=True,
+                         compile_filename='test.mm', use="OSX_FOUNDATION OSX_APPKIT",
+                         **kwargs
+                         )
+        try:
+            # Try standard paths first
+            check_sparkle()
+        except:
+            try:
+                # Try local path
+                Logs.info ("Check local version of Sparkle framework")
+                check_sparkle(cxxflags="-F%s/osx/Frameworks/" % conf.path.abspath(),
+                              linkflags="-F%s/osx/Frameworks/" % conf.path.abspath())
+                conf.env.HAVE_LOCAL_SPARKLE = 1
+            except:
+                import urllib, subprocess, os, shutil
+                if not os.path.exists('osx/Frameworks/Sparkle.framework'):
+                    # Download to local path and retry
+                    Logs.info ("Sparkle framework not found, trying to download it to 'build/'")
+
+                    urllib.urlretrieve ("https://github.com/sparkle-project/Sparkle/releases/download/1.16.0/Sparkle-1.16.0.tar.bz2", "build/Sparkle.tar.bz2")
+                    if os.path.exists('build/Sparkle.tar.bz2'):
+                        try:
+                            subprocess.check_call(['mkdir', 'build/Sparkle'])
+                            subprocess.check_call(['tar', 'xjf', 'build/Sparkle.tar.bz2', '-C', 'build/Sparkle'])
+                            os.remove("build/Sparkle.tar.bz2")
+                            if not os.path.exists("osx/Frameworks"):
+                                os.mkdir ("osx/Frameworks")
+                            os.rename("build/Sparkle/Sparkle.framework", "osx/Frameworks/Sparkle.framework")
+                            shutil.rmtree("build/Sparkle", ignore_errors=True)
+
+                            check_sparkle(cxxflags="-F%s/osx/Frameworks/" % conf.path.abspath(),
+                                          linkflags="-F%s/osx/Frameworks/" % conf.path.abspath())
+                            conf.env.HAVE_LOCAL_SPARKLE = 1
+                        except subprocess.CalledProcessError as e:
+                            conf.fatal("Cannot find Sparkle framework. Auto download failed: '%s' returned %s" % (' '.join(e.cmd), e.returncode))
+                        except:
+                            conf.fatal("Unknown Error happened when auto downloading Sparkle framework")
+
+        conf.env['LDFLAGS_OSX_SPARKLE'] += ['-Wl,-rpath,@loader_path/../Frameworks']
+        if conf.is_defined('HAVE_SPARKLE'):
+            conf.env.HAVE_SPARKLE = 1 # small cheat for wscript
diff --git a/make-deps.sh b/make-deps.sh
index 6a773ab..ae73214 100755
--- a/make-deps.sh
+++ b/make-deps.sh
@@ -9,16 +9,6 @@
 mkdir build 2>/dev/null || true
 path="$(pwd)"
 
-pushd build
-# wget https://github.com/sparkle-project/Sparkle/releases/download/1.14.0/Sparkle-1.14.0.tar.bz2
-wget https://github.com/sparkle-project/Sparkle/releases/download/1.16.0/Sparkle-1.16.0.tar.bz2
-mkdir Sparkle-1.16 || true
-pushd Sparkle-1.16
-tar xf ../Sparkle-1.16.0.tar.bz2
-popd
-mv Sparkle-1.16/Sparkle.framework .
-popd
-
 #######################################
 
 rm -Rf build/ndn-cxx
diff --git a/make-osx-bundle.py b/make-osx-bundle.py
index b7f98d4..6677d32 100755
--- a/make-osx-bundle.py
+++ b/make-osx-bundle.py
@@ -388,15 +388,13 @@
   a.copy_etc(['nfd.conf'])
   a.set_min_macosx_version('%s.0' % MIN_SUPPORTED_VERSION)
   a.macdeployqt()
-  a.copy_framework("build/Sparkle.framework")
+  a.copy_framework("osx/Frameworks/Sparkle.framework")
   a.done()
 
   # Sign our binaries, etc.
   if options.codesign:
     print ' * Signing binaries with identity `%s\'' % options.codesign
-    binaries = (a.bundle)
-
-    codesign(binaries)
+    codesign(a.bundle)
     print ''
 
   # Create diskimage
@@ -406,3 +404,8 @@
   d.symlink('/Applications', '/Applications')
   d.copy('build/%s/NDN.app' % MIN_SUPPORTED_VERSION, '/NDN.app')
   d.create()
+
+  if options.codesign:
+    print ' * Signing .dmg with identity `%s\'' % options.codesign
+    codesign(fn)
+    print ''
diff --git a/ndn-appcast.xml b/ndn-appcast.xml
deleted file mode 100644
index dd442ef..0000000
--- a/ndn-appcast.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
-  <channel>
-    <title>NDN.app Changleog</title>
-    <link>
-      http://named-data.net/binaries/ndn-appcast.xml
-    </link>
-    <description>Most recent changes with links to updates.</description>
-    <language>en</language>
-    <item>
-      <title>Version 0.1.0 (based on NFD version 0.4.0-22-g150b80d)</title>
-      <description><![CDATA[
-      <h2>Initial release</h2>
-      ]]>
-      </description>
-      <pubDate>Mon, 21 March 2016 15:38:00 +0000</pubDate>
-      <enclosure url="https://github.com/2nd-ndn-hackathon/nfd-binary-release/releases/download/0.1.0/NDN-0.1.0-NFD-0.4.0-22-g150b80d-10.10.dmg"
-                 sparkle:version="0.1.0"
-                 sparkle:dsaSignature="MC0CFQCSEv9IA78djidIEpQ4/X76rIEMIQIUXDRb1Stu8530bEJS4yZGUpymm1g="
-                 length="25623942"
-                 type="application/octet-stream" />
-    </item>
-    <item>
-      <title>Version 0.1.1 (based on NFD version 0.4.0-22-g150b80d)</title>
-      <description><![CDATA[
-      <h2>Minor fixes</h2>
-      <ul>
-      <li>Should work on all OSX 10.10+ systems</li>
-      <li>Fix bug with `ndn` wrapper</li>
-      </ul>
-      ]]>
-      </description>
-      <pubDate>Mon, 22 March 2016 14:42:00 +0000</pubDate>
-      <enclosure url="https://github.com/2nd-ndn-hackathon/nfd-binary-release/releases/download/0.1.1/NDN-0.1.1-NFD-0.4.0-22-g150b80d-10.10.dmg"
-                 sparkle:version="0.1.1"
-                 sparkle:dsaSignature="MC0CFAPv0czBOZJTfZMW7NHG4WsoU1tFAhUAnNVAlhd1pfc1IYtKEQmQ+oBXePQ="
-                 length="18643229"
-                 type="application/octet-stream" />
-    </item>
-  </channel>
-</rss>
diff --git a/ndn-control-center.xml b/ndn-control-center.xml
new file mode 100644
index 0000000..360e599
--- /dev/null
+++ b/ndn-control-center.xml
@@ -0,0 +1,22 @@
+<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
+  <channel>
+    <title>NDN App Changleog</title>
+    <link>
+      https://irl.cs.ucla.edu/~cawka/ndn-control-center.xml
+    </link>
+    <description>Most recent changes with links to updates.</description>
+    <language>en</language>
+    <item>
+      <title>Version 0.1.1 (based on NFD version 0.5.1)</title>
+      <description><![CDATA[
+      ]]>
+      </description>
+      <pubDate>Wed, 1 Feb 2017 20:05:00 +0000</pubDate>
+      <enclosure url="https://irl.cs.ucla.edu/~cawka/NDN-0.1.1.dmg"
+                 sparkle:version="0.1.1"
+                 sparkle:dsaSignature="MC0CFB9uEpOR2T0SNQK5tMdztcUAmq+8AhUAroo1gpugccjxwepKEp+VdgILeNc="
+                 length="30468966"
+                 type="application/octet-stream" />
+    </item>
+  </channel>
+</rss>
diff --git a/src/Info.plist b/src/Info.plist
index 40eda16..6bb818d 100644
--- a/src/Info.plist
+++ b/src/Info.plist
@@ -33,7 +33,7 @@
     <key>LSUIElement</key>
     <string>1</string>
     <key>SUFeedURL</key>
-    <string>http://named-data.net/binaries/ndn-appcast.xml</string>
+    <string>https://irl.cs.ucla.edu/~cawka/ndn-control-center.xml</string>
     <key>SUPublicDSAKeyFile</key>
     <string>ndn_sparkle_pub.pem</string>
 </dict>
diff --git a/src/main.cpp b/src/main.cpp
index d03f0cd..e39e70c 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -140,7 +140,7 @@
 
   ForwarderStatusModel m_forwarderStatusModel;
   FibStatusModel m_fibModel;
-  TrayMenu m_tray;
+  ncc::TrayMenu m_tray;
 };
 
 } // namespace ndn
diff --git a/src/osx-auto-update-sparkle.hpp b/src/osx-auto-update-sparkle.hpp
new file mode 100644
index 0000000..716f1dc
--- /dev/null
+++ b/src/osx-auto-update-sparkle.hpp
@@ -0,0 +1,53 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2013-2017, Regents of the University of California.
+ *
+ * This file is part of NFD Control Center.  See AUTHORS.md for complete list of NFD
+ * authors and contributors.
+ *
+ * NFD Control Center is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD Control Center 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with NFD
+ * Control Center, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NCC_OSX_AUTO_UPDATE_SPARKLE_HPP
+#define NCC_OSX_AUTO_UPDATE_SPARKLE_HPP
+
+#include "config.hpp"
+
+#ifndef OSX_BUILD
+#error "Cannot be included for non-OSX build"
+#endif // OSX_BUILD
+
+#include <string>
+#include <ndn-cxx/util/backports.hpp>
+
+namespace ndn {
+namespace ncc {
+
+class OsxAutoUpdateSparkle
+{
+public:
+  OsxAutoUpdateSparkle(const std::string& updateUrl);
+
+  ~OsxAutoUpdateSparkle();
+
+  void
+  checkForUpdates();
+
+private:
+  class Impl;
+  unique_ptr<Impl> m_impl;
+};
+
+} // namespace ncc
+} // namespace ndn
+
+#endif // NCC_OSX_AUTO_UPDATE_SPARKLE_HPP
diff --git a/src/osx-auto-update-sparkle.mm b/src/osx-auto-update-sparkle.mm
new file mode 100644
index 0000000..a4e5a0f
--- /dev/null
+++ b/src/osx-auto-update-sparkle.mm
@@ -0,0 +1,96 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2013-2017, Regents of the University of California.
+ *
+ * This file is part of NFD Control Center.  See AUTHORS.md for complete list of NFD
+ * authors and contributors.
+ *
+ * NFD Control Center is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD Control Center 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with NFD
+ * Control Center, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "osx-auto-update-sparkle.hpp"
+
+#import <AppKit/AppKit.h>
+#import <Foundation/Foundation.h>
+#import <Sparkle/Sparkle.h>
+
+@interface UpdaterDelegate : NSObject<SUUpdaterDelegate> {
+  ndn::ncc::OsxAutoUpdateSparkle* this_;
+}
+
+- (void)setSelf:(ndn::ncc::OsxAutoUpdateSparkle*)ptr;
+
+- (BOOL)updater:(SUUpdater *)updater
+        shouldPostponeRelaunchForUpdate:(SUAppcastItem *)update
+        untilInvoking:(NSInvocation *)invocation;
+
+@end
+
+@implementation UpdaterDelegate
+
+- (void)setSelf:(ndn::ncc::OsxAutoUpdateSparkle*)ptr
+{
+  this_ = ptr;
+}
+
+- (BOOL)updater:(SUUpdater *)updater
+        shouldPostponeRelaunchForUpdate:(SUAppcastItem *)update
+        untilInvoking:(NSInvocation *)invocation
+{
+  NSTask *task = [[NSTask alloc] init];
+  task.launchPath = @"/usr/bin/killall";
+  task.arguments = @[@"ndn-autoconfig", @"nfd"];
+  [task launch];
+  return NO;
+}
+
+@end
+
+namespace ndn {
+namespace ncc {
+
+class OsxAutoUpdateSparkle::Impl
+{
+public:
+  SUUpdater* m_updater;
+  UpdaterDelegate* delegate;
+};
+
+OsxAutoUpdateSparkle::OsxAutoUpdateSparkle(const std::string& updateUrl)
+  : m_impl(make_unique<Impl>())
+{
+  m_impl->m_updater = [[SUUpdater sharedUpdater] retain];
+  NSURL* url = [NSURL URLWithString:[NSString stringWithUTF8String:updateUrl.data()]];
+  [m_impl->m_updater setFeedURL:url];
+  [m_impl->m_updater setAutomaticallyChecksForUpdates:YES];
+  [m_impl->m_updater setUpdateCheckInterval:86400];
+
+  m_impl->delegate = [[UpdaterDelegate alloc] init];
+  [m_impl->delegate setSelf:this];
+  [m_impl->m_updater setDelegate:m_impl->delegate];
+}
+
+OsxAutoUpdateSparkle::~OsxAutoUpdateSparkle()
+{
+  [m_impl->m_updater release];
+  // presummably SUUpdater handles garbage collection
+}
+
+void
+OsxAutoUpdateSparkle::checkForUpdates()
+{
+  //[m_impl->m_updater checkForUpdatesInBackground];
+  [m_impl->m_updater checkForUpdates:nil];
+}
+
+} // namespace ncc
+} // namespace ndn
diff --git a/src/tray-menu.cpp b/src/tray-menu.cpp
index d4d2637..2ec3cb4 100644
--- a/src/tray-menu.cpp
+++ b/src/tray-menu.cpp
@@ -35,6 +35,7 @@
 #endif // OSX_BUILD
 
 namespace ndn {
+namespace ncc {
 
 TrayMenu::TrayMenu(QQmlContext* context, Face& face)
   : m_context(context)
@@ -42,24 +43,22 @@
   , m_menu(new QMenu(this))
   , m_entryPref(new QAction("Preferences...", m_menu))
   , m_entrySec(new QAction("Security...", m_menu))
+  , m_acProc(new QProcess())
+  , m_settings(new QSettings())
 #ifdef OSX_BUILD
   , m_entryEnableCli(new QAction("Enable Command Terminal Usage...", m_menu))
+  , m_checkForUpdates(new QAction("Check for updates", m_menu))
+  , m_sparkle("https://irl.cs.ucla.edu/~cawka/ndn-control-center.xml")
 #endif
   , m_entryQuit(new QAction("Quit", m_menu))
   , m_keyViewerDialog(new ncc::KeyViewerDialog)
   , m_face(face)
-  , m_acProc(new QProcess())
-  , m_settings(new QSettings())
 {
   connect(m_entryPref, SIGNAL(triggered()), this, SIGNAL(showApp()));
   connect(this, SIGNAL(showApp()), this, SLOT(showPref()));
   connect(m_entrySec, SIGNAL(triggered()), m_keyViewerDialog, SLOT(present()));
   connect(m_entryQuit, SIGNAL(triggered()), this, SLOT(quitApp()));
 
-#ifdef OSX_BUILD
-  connect(m_entryEnableCli, SIGNAL(triggered()), this, SLOT(enableCli()));
-#endif
-
   connect(this, SIGNAL(nfdActivityUpdate(bool)), this, SLOT(updateNfdActivityIcon(bool)),
           Qt::QueuedConnection);
 
@@ -69,7 +68,11 @@
   m_menu->addAction(m_entryPref);
   m_menu->addAction(m_entrySec);
 #ifdef OSX_BUILD
+  connect(m_entryEnableCli, SIGNAL(triggered()), this, SLOT(enableCli()));
   m_menu->addAction(m_entryEnableCli);
+
+  connect(m_checkForUpdates, SIGNAL(triggered()), this, SLOT(checkForUpdates()));
+  m_menu->addAction(m_checkForUpdates);
 #endif
   m_menu->addAction(m_entryQuit);
   m_tray = new QSystemTrayIcon(this);
@@ -284,7 +287,6 @@
   connect(addNewRoute,SIGNAL(finished(int)), addNewRoute, SLOT(deleteLater()));
   addNewRoute->start("bash", QStringList() << "-c" << cmd);
   std::cout << "Done" << std::endl;
-
 }
 
 void
@@ -347,4 +349,13 @@
 #endif
 }
 
+#ifdef OSX_BUILD
+void
+TrayMenu::checkForUpdates()
+{
+  m_sparkle.checkForUpdates();
+}
+#endif // OSX_BUILD
+
+} // namespace ncc
 } // namespace ndn
diff --git a/src/tray-menu.hpp b/src/tray-menu.hpp
index 3c7b0f0..841f068 100644
--- a/src/tray-menu.hpp
+++ b/src/tray-menu.hpp
@@ -39,10 +39,16 @@
 
 #include "key-viewer-dialog.hpp"
 
+#ifdef OSX_BUILD
+#include "osx-auto-update-sparkle.hpp"
+#endif // OSX_BUILD
+
 namespace ndn {
 
 class Face;
 
+namespace ncc {
+
 class TrayMenu : public QWidget
 {
   Q_OBJECT
@@ -113,6 +119,11 @@
   static void
   appendMsg(QString &target, QString source);
 
+#ifdef OSX_BUILD
+  void
+  checkForUpdates();
+#endif // OSX_BUILD
+
 private:
   QQmlContext* m_context;
   bool m_isNfdRunning;
@@ -125,7 +136,9 @@
   QSettings m_settings;
 #ifdef OSX_BUILD
   QAction* m_entryEnableCli;
-#endif
+  QAction* m_checkForUpdates;
+  OsxAutoUpdateSparkle m_sparkle;
+#endif // OSX_BUILD
 
   QAction* m_entryQuit;
 
@@ -133,6 +146,7 @@
   Face& m_face;
 };
 
+} // namespace ncc
 } // namespace ndn
 
 #endif // NCC_TRAY_MENU_HPP
diff --git a/wscript b/wscript
index f536815..a25a5ad 100644
--- a/wscript
+++ b/wscript
@@ -7,10 +7,10 @@
 
 def options(opt):
     opt.load('compiler_c compiler_cxx qt5 gnu_dirs')
-    opt.load('boost default-compiler-flags', tooldir='.waf-tools')
+    opt.load('boost osx-frameworks default-compiler-flags', tooldir='.waf-tools')
 
 def configure(conf):
-    conf.load('compiler_c compiler_cxx default-compiler-flags boost')
+    conf.load('compiler_c compiler_cxx default-compiler-flags boost osx-frameworks')
 
     if 'PKG_CONFIG_PATH' not in os.environ:
         conf.environ['PKG_CONFIG_PATH'] = Utils.subst_vars('${LIBDIR}/pkgconfig', conf.env)
@@ -55,7 +55,15 @@
         bld.install_files("${DATAROOTDIR}/nfd-control-center",
                           bld.path.ant_glob(['res/*']))
     else:
+        app.source += bld.path.ant_glob(['src/osx-*.mm', 'src/osx-*.cpp'])
+        app.use += " OSX_FOUNDATION OSX_APPKIT OSX_SPARKLE OSX_COREWLAN"
         app.target = "NFD Control Center"
         app.mac_app = True
         app.mac_plist = 'src/Info.plist'
         app.mac_files = [i.path_from(bld.path) for i in bld.path.ant_glob('res/**/*', excl='**/*.ai')]
+
+from waflib import TaskGen
+@TaskGen.extension('.mm')
+def m_hook(self, node):
+    """Alias .mm files to be compiled the same as .cpp files, gcc/clang will do the right thing."""
+    return self.create_compiled_task('cxx', node)