package com.nutomic.syncthingandroid.syncthing;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Environment;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import com.nutomic.syncthingandroid.BuildConfig;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import eu.chainfire.libsuperuser.Shell;
/**
* Runs the syncthing binary from command line, and prints its output to logcat.
*
* @see <a href="http://docs.syncthing.net/users/syncthing.html">Command Line Docs</a>
*/
public class SyncthingRunnable implements Runnable {
private static final String TAG = "SyncthingRunnable";
private static final String TAG_NATIVE = "SyncthingNativeCode";
private static final String TAG_NICE = "SyncthingRunnableIoNice";
private static final String TAG_KILL = "SyncthingRunnableKill";
public static final String UNIT_TEST_PATH = "was running";
private static final AtomicReference<Process> mSyncthing = new AtomicReference<>();
private final Context mContext;
private final String mSyncthingBinary;
private String[] mCommand;
private String mErrorLog;
public enum Command {
generate, // Generate keys, a config file and immediately exit.
main, // Run the main Syncthing application.
reset, // Reset Syncthing's indexes
}
/**
* Constructs instance.
*
* @param command Which type of Syncthing command to execute.
*/
public SyncthingRunnable(Context context, Command command) {
mContext = context;
mSyncthingBinary = mContext.getApplicationInfo().dataDir + "/" + SyncthingService.BINARY_NAME;
switch (command) {
case generate:
mCommand = new String[]{ mSyncthingBinary, "-generate", mContext.getFilesDir().toString() };
break;
case main:
mCommand = new String[]{ mSyncthingBinary, "-home", mContext.getFilesDir().toString(), "-no-browser" };
break;
case reset:
mCommand = new String[]{ mSyncthingBinary, "-home", mContext.getFilesDir().toString(), "-reset" };
break;
default:
Log.w(TAG, "Unknown command option");
}
}
/**
* Constructs instance.
*
* @param manualCommand The exact command to be executed on the shell. Used for tests only.
*/
public SyncthingRunnable(Context context, String[] manualCommand) {
mContext = context;
mSyncthingBinary = mContext.getApplicationInfo().dataDir + "/" + SyncthingService.BINARY_NAME;
mCommand = manualCommand;
}
@Override
public void run() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
int ret;
// Make sure Syncthing is executable
try {
ProcessBuilder pb = new ProcessBuilder("chmod", "500", mSyncthingBinary);
Process p = pb.start();
p.waitFor();
} catch (IOException|InterruptedException e) {
Log.w(TAG, "Failed to chmod Syncthing", e);
}
// Loop Syncthing
Process process = null;
// Potential fix for #498, keep the CPU running while native binary is running
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wakeLock = useWakeLock()
? pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)
: null;
try {
if (wakeLock != null)
wakeLock.acquire();
ProcessBuilder pb = (useRoot())
? new ProcessBuilder("su", "-c", TextUtils.join(" ", mCommand))
: new ProcessBuilder(mCommand);
Map<String, String> env = pb.environment();
// Set home directory to data folder for web GUI folder picker.
env.put("HOME", Environment.getExternalStorageDirectory().getAbsolutePath());
env.put("STTRACE", sp.getString("sttrace", ""));
File externalFilesDir = mContext.getExternalFilesDir(null);
if (externalFilesDir != null)
env.put("STGUIASSETS", externalFilesDir.getAbsolutePath() + "/gui");
env.put("STNORESTART", "1");
env.put("STNOUPGRADE", "1");
if (sp.getBoolean("use_tor", false)) {
env.put("all_proxy", "socks5://localhost:9050");
env.put("ALL_PROXY_NO_FALLBACK", "1");
}
process = pb.start();
mSyncthing.set(process);
mErrorLog = "";
Thread lInfo = log(process.getInputStream(), Log.INFO, true);
Thread lWarn = log(process.getErrorStream(), Log.WARN, true);
niceSyncthing();
ret = process.waitFor();
mSyncthing.set(null);
lInfo.join();
lWarn.join();
switch (ret) {
case 0:
case 2:
case 4:
// Valid exit codes, ignored.
break;
case 3:
// Restart if that was requested via Rest API call.
Log.i(TAG, "Restarting syncthing");
mContext.startService(new Intent(mContext, SyncthingService.class)
.setAction(SyncthingService.ACTION_RESTART));
break;
case 137:
// Ignore SIGKILL that we use to stop Syncthing.
break;
case 1:
// fallthrough
default:
// Report Syncthing crashes, using Exception in debug mode or log in release mode.
String message = "Syncthing binary crashed with error code " +
Integer.toString(ret) + ", output:\n" + mErrorLog;
if (BuildConfig.DEBUG)
throw new RuntimeException(message);
else
Log.e(TAG, message);
}
} catch (IOException | InterruptedException e) {
Log.e(TAG, "Failed to execute syncthing binary or read output", e);
} finally {
if (wakeLock != null)
wakeLock.release();
if (process != null)
process.destroy();
}
}
/**
* Returns true if root is available and enabled in settings.
*/
private boolean useRoot() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
return sp.getBoolean(SyncthingService.PREF_USE_ROOT, false) && Shell.SU.available();
}
/**
* Returns true if the experimental setting for using wake locks has been enabled in settings.
*/
private boolean useWakeLock() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
return sp.getBoolean(SyncthingService.PREF_USE_WAKE_LOCK, false);
}
/**
* Look for a running libsyncthing.so process and nice its IO.
*/
private void niceSyncthing() {
new Thread() {
public void run() {
Process nice = null;
DataOutputStream niceOut = null;
int ret = 1;
try {
Thread.sleep(1000); // Wait a second before getting the pid
nice = Runtime.getRuntime().exec((useRoot()) ? "su" : "sh");
niceOut = new DataOutputStream(nice.getOutputStream());
niceOut.writeBytes("set `ps | grep libsyncthing.so`\n");
niceOut.writeBytes("ionice $2 be 7\n"); // best-effort, low priority
niceOut.writeBytes("exit\n");
log(nice.getErrorStream(), Log.WARN, false);
niceOut.flush();
ret = nice.waitFor();
Log.i(TAG_NICE, "ionice performed on libsyncthing.so");
} catch (IOException | InterruptedException e) {
Log.e(TAG_NICE, "Failed to execute ionice binary", e);
} finally {
try {
if (niceOut != null) {
niceOut.close();
}
} catch (IOException e) {
Log.w(TAG_NICE, "Failed to close shell stream", e);
}
if (nice != null) {
nice.destroy();
}
if (ret != 0) {
Log.e(TAG_NICE, "Failed to set ionice " + Integer.toString(ret));
}
}
}
}.start();
}
/**
* Look for running libsyncthing.so processes and kill them.
* Try a SIGINT first, then try again with SIGKILL.
*/
public void killSyncthing() {
for (int i = 0; i < 2; i++) {
Process ps = null;
DataOutputStream psOut = null;
try {
ps = Runtime.getRuntime().exec((useRoot()) ? "su" : "sh");
psOut = new DataOutputStream(ps.getOutputStream());
psOut.writeBytes("ps | grep libsyncthing.so\n");
psOut.writeBytes("exit\n");
psOut.flush();
ps.waitFor();
InputStreamReader isr = new InputStreamReader(ps.getInputStream(), "UTF-8");
BufferedReader br = new BufferedReader(isr);
String id;
while ((id = br.readLine()) != null) {
id = id.trim().split("\\s+")[1];
killProcessId(id, i > 0);
}
} catch (IOException | InterruptedException e) {
Log.w(TAG_KILL, "Failed list Syncthing processes", e);
} finally {
try {
if (psOut != null)
psOut.close();
} catch (IOException e) {
Log.w(TAG_KILL, "Failed close the psOut stream", e);
}
if (ps != null) {
ps.destroy();
}
}
}
}
/**
* Kill a given process ID
*
* @param force Whether to use a SIGKILL.
*/
private void killProcessId(String id, boolean force) {
Process kill = null;
DataOutputStream killOut = null;
try {
kill = Runtime.getRuntime().exec((useRoot()) ? "su" : "sh");
killOut = new DataOutputStream(kill.getOutputStream());
if (!force) {
killOut.writeBytes("kill -SIGINT " + id + "\n");
killOut.writeBytes("sleep 1\n");
} else {
killOut.writeBytes("sleep 3\n");
killOut.writeBytes("kill -SIGKILL " + id + "\n");
}
killOut.writeBytes("exit\n");
killOut.flush();
kill.waitFor();
Log.i(TAG_KILL, "Killed Syncthing process "+id);
} catch (IOException | InterruptedException e) {
Log.w(TAG_KILL, "Failed to kill process id "+id, e);
} finally {
try {
if (killOut != null)
killOut.close();
} catch (IOException e) {
Log.w(TAG_KILL, "Failed close the killOut stream", e);}
if (kill != null) {
kill.destroy();
}
}
}
/**
* Logs the outputs of a stream to logcat and mNativeLog.
*
* @param is The stream to log.
* @param priority The priority level.
* @param saveLog True if the log should be stored to {@link #mErrorLog}.
*/
private Thread log(final InputStream is, final int priority, final boolean saveLog) {
Thread t = new Thread(() -> {
try {
InputStreamReader isr = new InputStreamReader(is, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
Log.println(priority, TAG_NATIVE, line);
if (saveLog)
mErrorLog += line + "\n";
}
} catch (IOException e) {
Log.w(TAG, "Failed to read Syncthing's command line output", e);
}
});
t.start();
return t;
}
}