diff --git a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
index 98d4214f14b2f4cb8094bc22f339bc9576ace4e0..91f0da2355b267336fcb201a1c882b36455e3464 100644
--- a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
+++ b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
@@ -62,6 +62,7 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
     private final Log _log;
     private final Collection<RegisteredUpdater> _registeredUpdaters;
     private final Collection<RegisteredChecker> _registeredCheckers;
+    private final Map<Integer, UpdatePostProcessor> _registeredPostProcessors;
     /** active checking tasks */
     private final Collection<UpdateTask> _activeCheckers;
     /** active updating tasks, pointing to the next ones to try */
@@ -95,6 +96,7 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
         _log = ctx.logManager().getLog(ConsoleUpdateManager.class);
         _registeredUpdaters = new ConcurrentHashSet<RegisteredUpdater>();
         _registeredCheckers = new ConcurrentHashSet<RegisteredChecker>();
+        _registeredPostProcessors = new ConcurrentHashMap<Integer, UpdatePostProcessor>(2);
         _activeCheckers = new ConcurrentHashSet<UpdateTask>();
         _downloaders = new ConcurrentHashMap<UpdateTask, List<RegisteredUpdater>>();
         _available = new ConcurrentHashMap<UpdateItem, VersionAvailable>();
@@ -756,6 +758,20 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
             _log.info("Unregistering " + rc);
         _registeredCheckers.remove(rc);
     }
+
+    /**
+     *  Register a post-processor for this UpdateType and SU3File file type.
+     *
+     *  @param type only ROUTER_SIGNED_SU3 and ROUTER_DEV_SU3 are currently supported
+     *  @param fileType a SU3File TYPE_xxx constant, 1-255, TYPE_ZIP not supported.
+     *  @since 0.9.51
+     */
+    public void register(UpdatePostProcessor upp, UpdateType type, int fileType) {
+        Integer key = Integer.valueOf(type.toString().hashCode() ^ fileType);
+        UpdatePostProcessor old = _registeredPostProcessors.put(key, upp);
+        if (old != null && _log.shouldLog(Log.WARN))
+            _log.warn("Duplicate registration " + upp);
+    }
     
     /**
      *  Called by the Updater, either after check() was called, or it found out on its own.
@@ -1102,7 +1118,8 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
         if (_log.shouldLog(Log.INFO))
             _log.info("Updater " + task + " for " + task.getType() + " complete");
         boolean rv = false;
-        switch (task.getType()) {
+        UpdateType utype = task.getType();
+        switch (utype) {
             case TYPE_DUMMY:
             case NEWS:
             case NEWS_SU3:
@@ -1110,13 +1127,13 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
                 break;
 
             case ROUTER_SIGNED:
-                rv = handleSudFile(task.getURI(), actualVersion, file);
+                rv = handleRouterFile(task.getURI(), actualVersion, file, utype);
                 if (rv)
                     notifyDownloaded(task.getType(), task.getID(), actualVersion);
                 break;
 
             case ROUTER_SIGNED_SU3:
-                rv = handleSu3File(task.getURI(), actualVersion, file);
+                rv = handleRouterFile(task.getURI(), actualVersion, file, utype);
                 if (rv)
                     notifyDownloaded(task.getType(), task.getID(), actualVersion);
                 break;
@@ -1130,7 +1147,7 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
                 break;
 
             case ROUTER_DEV_SU3:
-                rv = handleSu3File(task.getURI(), actualVersion, file);
+                rv = handleRouterFile(task.getURI(), actualVersion, file, utype);
                 if (rv) {
                     _context.router().saveConfig(PROP_DEV_SU3_AVAILABLE, null);
                     notifyDownloaded(task.getType(), task.getID(), actualVersion);
@@ -1325,47 +1342,48 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
     }
 
     /**
+     *  Process sud, su2, or su3.
+     *  Only for router updates.
      *
      *  @return success
-     */
-    private boolean handleSudFile(URI uri, String actualVersion, File f) {
-        return handleRouterFile(uri, actualVersion, f, false);
-    }
-
-    /**
-     *  @return success
-     *  @since 0.9.9
-     */
-    private boolean handleSu3File(URI uri, String actualVersion, File f) {
-        return handleRouterFile(uri, actualVersion, f, true);
-    }
-
-    /**
-     *  Process sud, su2, or su3
-     *  @return success
      *  @since 0.9.9
      */
-    private boolean handleRouterFile(URI uri, String actualVersion, File f, boolean isSU3) {
+    private boolean handleRouterFile(URI uri, String actualVersion, File f, UpdateType updateType) {
+        boolean isSU3 = updateType == ROUTER_SIGNED_SU3 || updateType == ROUTER_DEV_SU3;
         String url = uri.toString();
         updateStatus("<b>" + _t("Update downloaded") + "</b>");
         File to = new File(_context.getRouterDir(), Router.UPDATE_FILE);
-        String err;
+        String err = null;
         // Process the file
         if (isSU3) {
             SU3File up = new SU3File(_context, f);
-            File temp = new File(_context.getTempDir(), "su3out-" + _context.random().nextLong() + ".zip");
+            File temp = new File(_context.getTempDir(), "su3out-" + _context.random().nextLong());
             try {
                 if (up.verifyAndMigrate(temp)) {
                     String ver = up.getVersionString();
                     int type = up.getContentType();
-                    if (ver == null || VersionComparator.comp(RouterVersion.VERSION, ver) >= 0)
+                    if (ver == null || VersionComparator.comp(RouterVersion.VERSION, ver) >= 0) {
                         err = "Old version " + ver;
-                    else if (type != SU3File.CONTENT_ROUTER)
+                    } else if (type != SU3File.CONTENT_ROUTER) {
                         err = "Bad su3 content type " + type;
-                    else if (!FileUtil.copy(temp, to, true, false))
-                        err = "Failed copy to " + to;
-                    else
-                        err = null;   // success
+                    } else {
+                        int ftype = up.getFileType();
+                        if (ftype == SU3File.TYPE_ZIP) {
+                            // standard update, copy to i2pupdate.zip in config dir
+                            if (!FileUtil.copy(temp, to, true, false))
+                                err = "Failed copy to " + to;
+                        } else if ((ftype == SU3File.TYPE_DMG && SystemVersion.isMac()) ||
+                                   (ftype == SU3File.TYPE_EXE && SystemVersion.isWindows())) {
+                            Integer key = Integer.valueOf(updateType.toString().hashCode() ^ ftype);
+                            UpdatePostProcessor upp = _registeredPostProcessors.get(key);
+                            if (upp != null)
+                                upp.updateDownloadedandVerified(updateType, ftype, actualVersion, temp);
+                            else
+                                err = "Unsupported su3 file type " + ftype;
+                        } else {
+                            err = "Unsupported su3 file type " + ftype;
+                        }
+                    }
                 } else {
                     err = "Signature failed, signer " + DataHelper.stripHTML(up.getSignerString()) +
                           ' ' + up.getSigType();
@@ -1406,6 +1424,8 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
     }
 
     /**
+     *  Only for router updates
+     *
      *  @param Long.toString(timestamp)
      *  @return success
      */
@@ -1755,6 +1775,10 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
         buf.append("<div class=\"debug_container\">");
         toString(buf, _registeredUpdaters);
         buf.append("</div>");
+        buf.append("<h3>Registered PostProcessors</h3>");
+        buf.append("<div class=\"debug_container\">");
+        toString(buf, _registeredPostProcessors.values());
+        buf.append("</div>");
         buf.append("<h3>Active Checkers</h3>");
         buf.append("<div class=\"debug_container\">");
         toString(buf, _activeCheckers);
diff --git a/core/java/src/net/i2p/crypto/SU3File.java b/core/java/src/net/i2p/crypto/SU3File.java
index ca3a9cca39f5d3edbc63be62efaa109da4cd9314..ad31f0a4cd385d912ab0aa3a05ccc1a843901ff9 100644
--- a/core/java/src/net/i2p/crypto/SU3File.java
+++ b/core/java/src/net/i2p/crypto/SU3File.java
@@ -81,6 +81,10 @@ public class SU3File {
     public static final int TYPE_XML_GZ = 3;
     /** @since 0.9.28 */
     public static final int TYPE_TXT_GZ = 4;
+    /** @since 0.9.51 */
+    public static final int TYPE_DMG = 5;
+    /** @since 0.9.51 */
+    public static final int TYPE_EXE = 6;
 
     public static final int CONTENT_UNKNOWN = 0;
     public static final int CONTENT_ROUTER = 1;
@@ -703,7 +707,9 @@ public class SU3File {
                    "      HTML\t(code: 2)\n" +
                    "      XML_GZ\t(code: 3)\n" +
                    "      TXT_GZ\t(code: 4)\n" +
-                   "      (user defined)\t(code: 5-255)\n");
+                   "      DMG\t(code: 5)\n" +
+                   "      EXE\t(code: 6)\n" +
+                   "      (user defined)\t(code: 7-255)\n");
         return buf.toString();
     }
 
@@ -761,6 +767,10 @@ public class SU3File {
                 ftype = "HTML";
             else if (file._fileType == TYPE_XML_GZ)
                 ftype = "XML_GZ";
+            else if (file._fileType == TYPE_DMG)
+                ftype = "DMG";
+            else if (file._fileType == TYPE_EXE)
+                ftype = "EXE";
             else
                 ftype = Integer.toString(file._fileType);
                 System.out.println("FileType: " + ftype);
@@ -861,6 +871,10 @@ public class SU3File {
                 ft = TYPE_HTML;
             } else if (ftype.equalsIgnoreCase("XML_GZ")) {
                 ft = TYPE_XML_GZ;
+            } else if (ftype.equalsIgnoreCase("DMG")) {
+                ft = TYPE_DMG;
+            } else if (ftype.equalsIgnoreCase("EXE")) {
+                ft = TYPE_EXE;
             } else {
                 try {
                     ft = Integer.parseInt(ftype);
@@ -975,6 +989,12 @@ public class SU3File {
                   case TYPE_TXT_GZ:
                     sfx = ".txt.gz";
                     break;
+                  case TYPE_DMG:
+                    sfx = ".dmg";
+                    break;
+                  case TYPE_EXE:
+                    sfx = ".exe";
+                    break;
                   default:
                     sfx = ".extracted";
                     break;
diff --git a/core/java/src/net/i2p/update/UpdateManager.java b/core/java/src/net/i2p/update/UpdateManager.java
index 716c2fbace1d20c10d7bd659b77c96e27a285aab..c7cb492e1ce419917b7ccca2a04226313771047a 100644
--- a/core/java/src/net/i2p/update/UpdateManager.java
+++ b/core/java/src/net/i2p/update/UpdateManager.java
@@ -34,7 +34,16 @@ public interface UpdateManager {
     public void unregister(Updater updater, UpdateType type, UpdateMethod method);
 
     public void unregister(Checker checker, UpdateType type, UpdateMethod method);
-    
+
+    /**
+     *  Register a post-processor for this UpdateType and SU3File file type.
+     *
+     *  @param type only ROUTER_SIGNED_SU3 and ROUTER_DEV_SU3 are currently supported
+     *  @param fileType a SU3File TYPE_xxx constant, 1-255, TYPE_ZIP not supported.
+     *  @since 0.9.51
+     */
+    public void register(UpdatePostProcessor upp, UpdateType type, int fileType);
+
     public void start();
 
     public void shutdown();
diff --git a/core/java/src/net/i2p/update/UpdatePostProcessor.java b/core/java/src/net/i2p/update/UpdatePostProcessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..050a9aed04f12a1fc02e60f3197749c7c96ef19b
--- /dev/null
+++ b/core/java/src/net/i2p/update/UpdatePostProcessor.java
@@ -0,0 +1,50 @@
+package net.i2p.update;
+
+import java.io.IOException;
+import java.io.File;
+
+/**
+ *  An external class to handle complex processing of update files,
+ *  where necessary instead of simply copying i2pupdate.zip to the config dir.
+ *
+ *  @since 0.9.51
+ */
+public interface UpdatePostProcessor {
+    
+    /**
+     *  Notify the post-processor that an update has been downloaded and verified.
+     *  The version will be higher than the currently-installed version.
+     *
+     *  This method MUST immediately postprocess, copy, or rename the file, which
+     *  will be located in the temporary directory.
+     *  Caller will delete the file if it remains, immediately after this method returns.
+     *
+     *  This method MUST throw an IOException on all errors. The IOException will be
+     *  displayed to the user in the console, so it should be clear.
+     *
+     *  This method must not trigger the shutdown itself.
+     *  Caller will trigger the shutdown if so configured.
+     *
+     *  If the post-processor needs to perform any actions at shutdown, it should
+     *  call I2PAppContext.addShutdownTask() or RouterContext.addFinalShutdownTask().
+     *  See javadocs for restrictions on final shutdown tasks.
+     *  Note that the router's temporary directory is deleted at shutdown,
+     *  BEFORE the final shutdown tasks are run.
+     *
+     *  After this call, the router will do a graceful shutdown if so configured,
+     *  or will notify the user in the console to manually shut down the router.
+     *  Therefore, the shutdown may happen immediately, or be delayed for 10 minutes,
+     *  or may be hours, days, or weeks later.
+     *
+     *  In rare cases, a newer update may be downloaded before the shutdown
+     *  for the first update, and this method may be called again with the newer version.
+     *  Implementers must take care to properly handle multiple calls.
+     *
+     *  @param type only ROUTER_SIGNED_SU3 and ROUTER_DEV_SU3 are currently supported
+     *  @param fileType a TYPE_xxx file type code from the SU3File, 0-255
+     *  @param version the version string from the SU3File
+     *  @param file in the temp directory, as extracted from the validated su3 file
+     *  @throws IOException on all errors, message will be displayed to the user
+     */
+    public void updateDownloadedandVerified(UpdateType type, int fileType, String version, File file) throws IOException;
+}