From 0e6e77c476586f0f6ec37da0b0996631dfbe9cd0 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Sun, 24 Jan 2016 14:30:23 +0000 Subject: Implement persistent disk log cache. (closes #340) --- .../blinkt/openvpn/core/ICSOpenVPNApplication.java | 2 + .../de/blinkt/openvpn/core/LogFileHandler.java | 140 +++++++++++++++++++++ .../de/blinkt/openvpn/core/OpenVPNService.java | 1 + .../java/de/blinkt/openvpn/core/VpnStatus.java | 112 +++++++++++------ main/src/main/res/menu/logmenu.xml | 1 - 5 files changed, 215 insertions(+), 41 deletions(-) create mode 100644 main/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java (limited to 'main/src') diff --git a/main/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java b/main/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java index 2420194d..0ddd01b3 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java +++ b/main/src/main/java/de/blinkt/openvpn/core/ICSOpenVPNApplication.java @@ -37,5 +37,7 @@ public class ICSOpenVPNApplication extends Application { if (BuildConfig.DEBUG) { //ACRA.init(this); } + + VpnStatus.initLogCache(getApplicationContext().getCacheDir()); } } diff --git a/main/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java b/main/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java new file mode 100644 index 00000000..e3422df0 --- /dev/null +++ b/main/src/main/java/de/blinkt/openvpn/core/LogFileHandler.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2012-2015 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Created by arne on 23.01.16. + */ +class LogFileHandler extends Handler { + static final int TRIM_LOG_FILE = 100; + static final int FLUSH_TO_DISK = 101; + static final int LOG_INIT = 102; + public static final int LOG_MESSAGE = 103; + private static FileOutputStream mLogFile; + private static BufferedOutputStream mBufLogfile; + + public static final String LOGFILE_NAME = "logcache.dat"; + + + public LogFileHandler(Looper looper) { + super(looper); + } + + + @Override + public void handleMessage(Message msg) { + try { + if (msg.what == LOG_INIT) { + readLogCache((File) msg.obj); + openLogFile((File) msg.obj); + } else if (msg.what == LOG_MESSAGE && msg.obj instanceof VpnStatus.LogItem) { + // Ignore log messages if not yet initialized + if (mLogFile == null) + return; + writeLogItemToDisk((VpnStatus.LogItem) msg.obj); + } else if (msg.what == TRIM_LOG_FILE) { + trimLogFile(); + for (VpnStatus.LogItem li : VpnStatus.getlogbuffer()) + writeLogItemToDisk(li); + } else if (msg.what == FLUSH_TO_DISK) { + flushToDisk(); + } + + } catch (IOException e) { + e.printStackTrace(); + VpnStatus.logError("Error during log cache: " + msg.what); + VpnStatus.logException(e); + } + + } + + private void flushToDisk() throws IOException { + mLogFile.flush(); + } + + private static void trimLogFile() { + try { + mBufLogfile.flush(); + mLogFile.getChannel().truncate(0); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + + private void writeLogItemToDisk(VpnStatus.LogItem li) throws IOException { + Parcel p = Parcel.obtain(); + li.writeToParcel(p, 0); + // We do not really care if the log cache breaks between Android upgrades, + // write binary format to disc + byte[] liBytes = p.marshall(); + + mLogFile.write(liBytes.length & 0xff); + mLogFile.write(liBytes.length >> 8); + mLogFile.write(liBytes); + p.recycle(); + } + + private void openLogFile (File cacheDir) throws FileNotFoundException { + File logfile = new File(cacheDir, LOGFILE_NAME); + mLogFile = new FileOutputStream(logfile); + mBufLogfile = new BufferedOutputStream(mLogFile); + } + + private void readLogCache(File cacheDir) { + File logfile = new File(cacheDir, LOGFILE_NAME); + + if (!logfile.exists() || !logfile.canRead()) + return; + + VpnStatus.logDebug("Reread log items from cache file"); + + try { + BufferedInputStream logFile = new BufferedInputStream(new FileInputStream(logfile)); + + byte[] buf = new byte[8192]; + int read = logFile.read(buf, 0, 2); + + while (read > 0) { + // Marshalled LogItem + int len = (0xff & buf[0]) | buf[1] << 8; + + read = logFile.read(buf, 0, len); + + Parcel p = Parcel.obtain(); + p.unmarshall(buf, 0, read); + p.setDataPosition(0); + VpnStatus.LogItem li = VpnStatus.LogItem.CREATOR.createFromParcel(p); + VpnStatus.newLogItem(li, true); + p.recycle(); + + //Next item + read = logFile.read(buf, 0, 2); + } + + } catch (java.io.IOException e) { + VpnStatus.logError("Reading cached logfile failed"); + VpnStatus.logException(e); + e.printStackTrace(); + // ignore reading file error + } + } + +} diff --git a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java index 85336ac3..55d4f55f 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -493,6 +493,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } // Just in case unregister for state VpnStatus.removeStateListener(this); + VpnStatus.flushLog(); } diff --git a/main/src/main/java/de/blinkt/openvpn/core/VpnStatus.java b/main/src/main/java/de/blinkt/openvpn/core/VpnStatus.java index 3ff41eee..c2085502 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/VpnStatus.java +++ b/main/src/main/java/de/blinkt/openvpn/core/VpnStatus.java @@ -12,6 +12,8 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; import android.os.Build; +import android.os.HandlerThread; +import android.os.Message; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -98,8 +100,8 @@ public class VpnStatus { break; } - while(message.endsWith(",")) - message = message.substring(0, message.length()-1); + while (message.endsWith(",")) + message = message.substring(0, message.length() - 1); String prefix = c.getString(mLastStateresid) + ":"; String status = mLaststate; @@ -112,6 +114,16 @@ public class VpnStatus { } + public static void initLogCache(File cacheDir) { + Message m = mLogFileHandler.obtainMessage(LogFileHandler.LOG_INIT, cacheDir); + mLogFileHandler.sendMessage(m); + + } + + public static void flushLog() { + mLogFileHandler.sendEmptyMessage(LogFileHandler.FLUSH_TO_DISK); + } + public enum ConnectionStatus { LEVEL_CONNECTED, LEVEL_VPNPAUSED, @@ -122,7 +134,7 @@ public class VpnStatus { LEVEL_START, LEVEL_AUTH_FAILED, LEVEL_WAITING_FOR_USER_INPUT, - UNKNOWN_LEVEL; + UNKNOWN_LEVEL } public enum LogLevel { @@ -167,18 +179,24 @@ public class VpnStatus { private static ConnectionStatus mLastLevel = ConnectionStatus.LEVEL_NOTCONNECTED; + private static final LogFileHandler mLogFileHandler; + static { logbuffer = new LinkedList<>(); logListener = new Vector<>(); stateListener = new Vector<>(); byteCountListener = new Vector<>(); + + HandlerThread mHandlerThread = new HandlerThread("LogFileWriter", Thread.MIN_PRIORITY); + mHandlerThread.start(); + mLogFileHandler = new LogFileHandler(mHandlerThread.getLooper()); + logInformation(); + } public static class LogItem implements Parcelable { - - private Object[] mArgs = null; private String mMessage = null; private int mRessourceId; @@ -350,11 +368,6 @@ public class VpnStatus { } } - public void saveLogToDisk(Context c) { - - File logOut = new File(c.getCacheDir(), "log.xml"); - } - public interface LogListener { void newLog(LogItem logItem); } @@ -375,6 +388,7 @@ public class VpnStatus { public synchronized static void clearLog() { logbuffer.clear(); logInformation(); + mLogFileHandler.sendEmptyMessage(LogFileHandler.TRIM_LOG_FILE); } private static void logInformation() { @@ -409,32 +423,34 @@ public class VpnStatus { } private static int getLocalizedState(String state) { - if (state.equals("CONNECTING")) - return R.string.state_connecting; - else if (state.equals("WAIT")) - return R.string.state_wait; - else if (state.equals("AUTH")) - return R.string.state_auth; - else if (state.equals("GET_CONFIG")) - return R.string.state_get_config; - else if (state.equals("ASSIGN_IP")) - return R.string.state_assign_ip; - else if (state.equals("ADD_ROUTES")) - return R.string.state_add_routes; - else if (state.equals("CONNECTED")) - return R.string.state_connected; - else if (state.equals("DISCONNECTED")) - return R.string.state_disconnected; - else if (state.equals("RECONNECTING")) - return R.string.state_reconnecting; - else if (state.equals("EXITING")) - return R.string.state_exiting; - else if (state.equals("RESOLVE")) - return R.string.state_resolve; - else if (state.equals("TCP_CONNECT")) - return R.string.state_tcp_connect; - else - return R.string.unknown_state; + switch (state) { + case "CONNECTING": + return R.string.state_connecting; + case "WAIT": + return R.string.state_wait; + case "AUTH": + return R.string.state_auth; + case "GET_CONFIG": + return R.string.state_get_config; + case "ASSIGN_IP": + return R.string.state_assign_ip; + case "ADD_ROUTES": + return R.string.state_add_routes; + case "CONNECTED": + return R.string.state_connected; + case "DISCONNECTED": + return R.string.state_disconnected; + case "RECONNECTING": + return R.string.state_reconnecting; + case "EXITING": + return R.string.state_exiting; + case "RESOLVE": + return R.string.state_resolve; + case "TCP_CONNECT": + return R.string.state_tcp_connect; + default: + return R.string.unknown_state; + } } @@ -536,17 +552,33 @@ public class VpnStatus { newLogItem(new LogItem(LogLevel.DEBUG, resourceId, args)); } + private static void newLogItem(LogItem logItem) { + newLogItem(logItem, false); + } + + + synchronized static void newLogItem(LogItem logItem, boolean cachedLine) { + if (cachedLine) { + logbuffer.addFirst(logItem); + } else { + logbuffer.addLast(logItem); + Message m = mLogFileHandler.obtainMessage(LogFileHandler.LOG_MESSAGE, logItem); + mLogFileHandler.sendMessage(m); + } + + if (logbuffer.size() > MAXLOGENTRIES + MAXLOGENTRIES / 2) { + while (logbuffer.size() > MAXLOGENTRIES) + logbuffer.removeFirst(); + mLogFileHandler.sendMessage(mLogFileHandler.obtainMessage(LogFileHandler.TRIM_LOG_FILE)); + } - private synchronized static void newLogItem(LogItem logItem) { - logbuffer.addLast(logItem); - if (logbuffer.size() > MAXLOGENTRIES) - logbuffer.removeFirst(); for (LogListener ll : logListener) { ll.newLog(logItem); } } + public static void logError(String msg) { newLogItem(new LogItem(LogLevel.ERROR, msg)); diff --git a/main/src/main/res/menu/logmenu.xml b/main/src/main/res/menu/logmenu.xml index a588081d..b8c493bc 100644 --- a/main/src/main/res/menu/logmenu.xml +++ b/main/src/main/res/menu/logmenu.xml @@ -39,5 +39,4 @@ android:icon="@drawable/ic_menu_edit" android:showAsAction="withText|ifRoom" android:title="@string/edit_vpn"/> - \ No newline at end of file -- cgit v1.2.3