From c4a3159b3399745db76cf20327a6f6e1e4401172 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Thu, 30 Aug 2012 14:06:06 +0000
Subject: [PATCH] Replace ident log with new, general-purpose event log. Use
 for stops, starts, and updates, and others. Mark all restarts on graphs using
 the event log.

---
 .../net/i2p/router/web/SummaryRenderer.java   |  18 ++-
 router/java/src/net/i2p/router/Router.java    |  28 ++--
 .../net/i2p/router/tasks/RouterWatchdog.java  |   2 +
 .../net/i2p/router/tasks/ShutdownHook.java    |   3 +
 .../src/net/i2p/router/util/EventLog.java     | 145 ++++++++++++++++++
 5 files changed, 182 insertions(+), 14 deletions(-)
 create mode 100644 router/java/src/net/i2p/router/util/EventLog.java

diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java
index 3a1676ea23..494e93de83 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java
@@ -7,6 +7,7 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.Map;
 
 import javax.imageio.ImageIO;
 import javax.imageio.stream.ImageOutputStream;
@@ -15,6 +16,7 @@ import javax.imageio.stream.MemoryCacheImageOutputStream;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
+import net.i2p.router.util.EventLog;
 import net.i2p.util.Log;
 
 import org.jrobin.core.RrdException;
@@ -137,9 +139,11 @@ class SummaryRenderer {
                 // Strings.java
                 descr = _(_listener.getRate().getRateStat().getDescription());
             }
-            long started = ((RouterContext)_context).router().getWhenStarted();
-            if (started > start && started < end)
-                def.vrule(started / 1000, RESTART_BAR_COLOR, _("Restart"), 4.0f);
+
+            //long started = ((RouterContext)_context).router().getWhenStarted();
+            //if (started > start && started < end)
+            //    def.vrule(started / 1000, RESTART_BAR_COLOR, _("Restart"), 4.0f);
+
             def.datasource(plotName, path, plotName, SummaryListener.CF, _listener.getBackendName());
             if (descr.length() > 0)
                 def.area(plotName, Color.BLUE, descr + "\\r");
@@ -151,6 +155,14 @@ class SummaryRenderer {
                 def.gprint(plotName, "LAST", ' ' + _("now") + ": %.2f %S\\r");
                 // '07-Jul 21:09 UTC' with month name in the system locale
                 SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM HH:mm");
+                Map<Long, String> events = ((RouterContext)_context).router().eventLog().getEvents(EventLog.STARTED, start);
+                for (Map.Entry<Long, String> event : events.entrySet()) {
+                    long started = event.getKey().longValue();
+                    if (started > start && started < end) {
+                        String legend = _("Restart") + ' ' + sdf.format(new Date(started)) + " UTC " + event.getValue() + "\\r";
+                        def.vrule(started / 1000, RESTART_BAR_COLOR, legend, 4.0f);
+                    }
+                }
                 def.comment(sdf.format(new Date(start)) + " -- " + sdf.format(new Date(end)) + " UTC\\r");
             }
             if (!showCredit)
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 442c8e78af..f270f18b75 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -40,6 +40,7 @@ import net.i2p.router.startup.WorkingDir;
 import net.i2p.router.tasks.*;
 import net.i2p.router.transport.FIFOBandwidthLimiter;
 import net.i2p.router.transport.udp.UDPTransport;
+import net.i2p.router.util.EventLog;
 import net.i2p.stat.RateStat;
 import net.i2p.stat.StatManager;
 import net.i2p.util.ByteCache;
@@ -77,6 +78,7 @@ public class Router implements RouterClock.ClockShiftListener {
     private I2PThread _gracefulShutdownDetector;
     private RouterWatchdog _watchdog;
     private Thread _watchdogThread;
+    private final EventLog _eventLog;
     
     public final static String PROP_CONFIG_FILE = "router.configLocation";
     
@@ -100,6 +102,7 @@ public class Router implements RouterClock.ClockShiftListener {
     public final static String PROP_KEYS_FILENAME_DEFAULT = "router.keys";
     public final static String PROP_SHUTDOWN_IN_PROGRESS = "__shutdownInProgress";
     public final static String DNS_CACHE_TIME = "" + (5*60);
+    private static final String EVENTLOG = "eventlog.txt";
         
     private static final String originalTimeZoneID;
     static {
@@ -219,12 +222,14 @@ public class Router implements RouterClock.ClockShiftListener {
         // i2p.dir.pid defaults to i2p.dir.router
         // i2p.dir.base defaults to user.dir == $CWD
         _context = new RouterContext(this, envProps);
+        _eventLog = new EventLog(_context, new File(_context.getRouterDir(), EVENTLOG));
 
         // This is here so that we can get the directory location from the context
         // for the ping file
         // Check for other router but do not start a thread yet so the update doesn't cause
         // a NCDFE
         if (!isOnlyRouterRunning()) {
+            _eventLog.addEvent(EventLog.ABORTED, "Another router running");
             System.err.println("ERROR: There appears to be another router already running!");
             System.err.println("       Please make sure to shut down old instances before starting up");
             System.err.println("       a new one.  If you are positive that no other instance is running,");
@@ -410,6 +415,7 @@ public class Router implements RouterClock.ClockShiftListener {
     public void runRouter() {
         if (_isAlive)
             throw new IllegalStateException();
+        _eventLog.addEvent(EventLog.STARTED, RouterVersion.FULL_VERSION);
         startupStuff();
         _isAlive = true;
         _started = _context.clock().now();
@@ -631,6 +637,13 @@ public class Router implements RouterClock.ClockShiftListener {
         return Certificate.NULL_CERT;
     }
     
+    /**
+     *  @since 0.9.3
+     */
+    public EventLog eventLog() {
+        return _eventLog;
+    }
+    
     /**
      * Ugly list of files that we need to kill if we are building a new identity
      *
@@ -646,7 +659,6 @@ public class Router implements RouterClock.ClockShiftListener {
                                                                  "sessionKeys.dat"     // no longer used
                                                                };
 
-    static final String IDENTLOG = "identlog.txt";
     public void killKeys() {
         //new Exception("Clearing identity files").printStackTrace();
         int remCount = 0;
@@ -671,18 +683,10 @@ public class Router implements RouterClock.ClockShiftListener {
         }
 
         if (remCount > 0) {
-            FileOutputStream log = null;
-            try {
-                log = new FileOutputStream(new File(_context.getRouterDir(), IDENTLOG), true);
-                log.write((new Date() + ": Old router identity keys cleared\n").getBytes());
-            } catch (IOException ioe) {
-                // ignore
-            } finally {
-                if (log != null)
-                    try { log.close(); } catch (IOException ioe) {}
-            }
+            _eventLog.addEvent(EventLog.REKEYED);
         }
     }
+
     /**
      * Rebuild a new identity the hard way - delete all of our old identity 
      * files, then reboot the router.
@@ -872,6 +876,7 @@ public class Router implements RouterClock.ClockShiftListener {
             }
         }
         _context.getFinalShutdownTasks().clear();
+        _eventLog.addEvent(EventLog.STOPPED, Integer.toString(exitCode));
 
         if (_killVMOnEnd) {
             try { Thread.sleep(1000); } catch (InterruptedException ie) {}
@@ -1140,6 +1145,7 @@ public class Router implements RouterClock.ClockShiftListener {
                 // Set the last version to the current version, since 0.8.13
                 _config.put("router.previousVersion", RouterVersion.VERSION);
                 saveConfig();
+                _eventLog.addEvent(EventLog.UPDATED);
                 ok = FileUtil.extractZip(updateFile, _context.getBaseDir());
             }
 
diff --git a/router/java/src/net/i2p/router/tasks/RouterWatchdog.java b/router/java/src/net/i2p/router/tasks/RouterWatchdog.java
index 7cc029b77a..213721f9f2 100644
--- a/router/java/src/net/i2p/router/tasks/RouterWatchdog.java
+++ b/router/java/src/net/i2p/router/tasks/RouterWatchdog.java
@@ -6,6 +6,7 @@ import net.i2p.data.DataHelper;
 import net.i2p.router.Job;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
+import net.i2p.router.util.EventLog;
 import net.i2p.stat.Rate;
 import net.i2p.stat.RateStat;
 import net.i2p.util.ShellCommand;
@@ -107,6 +108,7 @@ public class RouterWatchdog implements Runnable {
             _log.error("Memory: " + DataHelper.formatSize(used) + '/' + DataHelper.formatSize(max));
             if (_consecutiveErrors == 1) {
                 _log.log(Log.CRIT, "Router appears hung, or there is severe network congestion.  Watchdog starts barking!");
+                 _context.router().eventLog().addEvent(EventLog.WATCHDOG);
                 // This works on linux...
                 // It won't on windows, and we can't call i2prouter.bat either, it does something
                 // completely different...
diff --git a/router/java/src/net/i2p/router/tasks/ShutdownHook.java b/router/java/src/net/i2p/router/tasks/ShutdownHook.java
index ae8e38d7a4..d3db8f8765 100644
--- a/router/java/src/net/i2p/router/tasks/ShutdownHook.java
+++ b/router/java/src/net/i2p/router/tasks/ShutdownHook.java
@@ -10,6 +10,8 @@ package net.i2p.router.tasks;
 
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
+import net.i2p.router.RouterVersion;
+import net.i2p.router.util.EventLog;
 import net.i2p.util.Log;
 
 /**
@@ -35,6 +37,7 @@ public class ShutdownHook extends Thread {
         // Needed to make the wrapper happy, otherwise it gets confused
         // and thinks we haven't shut down, possibly because it
         // prevents other shutdown hooks from running
+        _context.router().eventLog().addEvent(EventLog.CRASHED, RouterVersion.FULL_VERSION);
         _context.router().setKillVMOnEnd(false);
         _context.router().shutdown2(Router.EXIT_HARD);
     }
diff --git a/router/java/src/net/i2p/router/util/EventLog.java b/router/java/src/net/i2p/router/util/EventLog.java
new file mode 100644
index 0000000000..9a2d9d20e1
--- /dev/null
+++ b/router/java/src/net/i2p/router/util/EventLog.java
@@ -0,0 +1,145 @@
+package net.i2p.router.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import net.i2p.I2PAppContext;
+import net.i2p.util.SecureFileOutputStream;
+
+/**
+ *  Simple event logger for occasional events,
+ *  with caching for reads.
+ *  Does not keep the file open.
+ *  @since 0.9.3
+ */
+public class EventLog {
+
+    private final I2PAppContext _context;
+    private final File _file;
+    /** event to cached map */
+    private final Map<String, SortedMap<Long, String>> _cache;
+    /** event to starting time of cached map */
+    private final Map<String, Long> _cacheTime;
+
+    /** for convenience, not required */
+    public static final String ABORTED = "aborted";
+    public static final String CHANGE_IP = "changeIP";
+    public static final String CHANGE_PORT = "changePort";
+    public static final String CLOCK_SHIFT = "clockShift";
+    public static final String CRASHED = "crashed";
+    public static final String INSTALLED = "installed";
+    public static final String INSTALL_FAILED = "intallFailed";
+    public static final String NEW_IDENT = "newIdent";
+    public static final String REKEYED = "rekeyed";
+    public static final String SOFT_RESTART = "softRestart";
+    public static final String STARTED = "started";
+    public static final String STOPPED = "stopped";
+    public static final String UPDATED = "updated";
+    public static final String WATCHDOG = "watchdog";
+
+    /**
+     *  @param file must be absolute
+     *  @throws IllegalArgumentException if not absolute
+     */
+    public EventLog(I2PAppContext ctx, File file) {
+        if (!file.isAbsolute())
+            throw new IllegalArgumentException();
+        _context = ctx;
+        _file = file;
+        _cache = new HashMap(4);
+        _cacheTime = new HashMap(4);
+    }
+
+    /**
+     *  Append an event. Fails silently.
+     *  @param event no spaces, e.g. "started"
+     *  @throws IllegalArgumentException if event contains a space or newline
+     */
+    public void addEvent(String event) {
+        addEvent(event, null);
+    }
+
+    /**
+     *  Append an event. Fails silently.
+     *  @param event no spaces or newlines, e.g. "started"
+     *  @param info no newlines, may be blank or null
+     *  @throws IllegalArgumentException if event contains a space or either contains a newline
+     */
+    public synchronized void addEvent(String event, String info) {
+        if (event.contains(" ") || event.contains("\n") ||
+            (info != null && info.contains("\n")))
+            throw new IllegalArgumentException();
+        _cache.remove(event);
+        _cacheTime.remove(event);
+        OutputStream out = null;
+        try {
+            out = new SecureFileOutputStream(_file, true);
+            StringBuilder buf = new StringBuilder(128);
+            buf.append(_context.clock().now()).append(' ').append(event);
+            if (info != null && info.length() > 0)
+                buf.append(' ').append(info);
+            buf.append('\n');
+            out.write(buf.toString().getBytes("UTF-8"));
+        } catch (IOException ioe) {
+        } finally {
+            if (out != null) try { out.close(); } catch (IOException ioe) {}
+        }
+    }
+
+    /**
+     *  Caches.
+     *  Fails silently.
+     *  @param event matching this event only, case sensitive
+     *  @param since since this time, 0 for all
+     *  @return non-null, Map of times to (possibly empty) info strings, sorted, earliest first, unmodifiable
+     */
+    public synchronized SortedMap<Long, String> getEvents(String event, long since) {
+        SortedMap<Long, String> rv = _cache.get(event);
+        if (rv != null) {
+            Long cacheTime = _cacheTime.get(event);
+            if (cacheTime != null) {
+                if (since >= cacheTime.longValue())
+                    return rv.tailMap(Long.valueOf(since));
+            }
+        }
+        rv = new TreeMap();
+        InputStream in = null;
+        try {
+            in = new FileInputStream(_file);
+            BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
+            String line = null;
+            while ( (line = br.readLine()) != null) {
+                try {
+                    String[] s = line.split(" ", 3);
+                    if (!s[1].equals(event))
+                        continue;
+                    long time = Long.parseLong(s[0]);
+                    if (time <= since)
+                        continue;
+                    Long ltime = Long.valueOf(time);
+                    String info = s.length > 2 ? s[2] : "";
+                    rv.put(time, info);
+                } catch (IndexOutOfBoundsException ioobe) {
+                } catch (NumberFormatException nfe) {
+                }
+            }
+            rv = Collections.unmodifiableSortedMap(rv);
+            _cache.put(event, rv);
+            _cacheTime.put(event, Long.valueOf(since));
+        } catch (IOException ioe) {
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+        }
+        return rv;
+    }
+}
-- 
GitLab