Added option to run syncthing as root (fixes #48).

This commit is contained in:
Felix Ableitner 2015-05-17 21:12:24 +02:00
parent f0daeb0cf8
commit 2fb425b441
6 changed files with 76 additions and 31 deletions

View File

@ -19,13 +19,15 @@ apply plugin: 'com.github.ben-manes.versions'
repositories { repositories {
mavenCentral() mavenCentral()
maven { maven {
url { url 'https://raw.github.com/kolavar/android-support-v4-preferencefragment/master/maven-repository/'
'https://raw.github.com/kolavar/android-support-v4-preferencefragment/master/maven-repository/'
} }
maven {
url 'http://jcenter.bintray.com'
} }
} }
dependencies { dependencies {
compile 'eu.chainfire:libsuperuser:1.0.0.201504231659'
compile 'com.android.support:appcompat-v7:22.0.0' compile 'com.android.support:appcompat-v7:22.0.0'
compile 'com.android.support:support-v4-preferencefragment:1.0.0@aar' compile 'com.android.support:support-v4-preferencefragment:1.0.0@aar'
androidTestCompile 'com.squareup.okhttp:mockwebserver:2.3.0' androidTestCompile 'com.squareup.okhttp:mockwebserver:2.3.0'

View File

@ -25,6 +25,8 @@ import com.nutomic.syncthingandroid.activities.SyncthingActivity;
import com.nutomic.syncthingandroid.syncthing.RestApi; import com.nutomic.syncthingandroid.syncthing.RestApi;
import com.nutomic.syncthingandroid.syncthing.SyncthingService; import com.nutomic.syncthingandroid.syncthing.SyncthingService;
import eu.chainfire.libsuperuser.Shell;
public class SettingsFragment extends PreferenceFragment public class SettingsFragment extends PreferenceFragment
implements SyncthingActivity.OnServiceConnectedListener, implements SyncthingActivity.OnServiceConnectedListener,
SyncthingService.OnApiChangeListener, Preference.OnPreferenceChangeListener, SyncthingService.OnApiChangeListener, Preference.OnPreferenceChangeListener,
@ -126,6 +128,7 @@ public class SettingsFragment extends PreferenceFragment
mSyncOnlyCharging = (CheckBoxPreference) mSyncOnlyCharging = (CheckBoxPreference)
findPreference(SyncthingService.PREF_SYNC_ONLY_CHARGING); findPreference(SyncthingService.PREF_SYNC_ONLY_CHARGING);
mSyncOnlyWifi = (CheckBoxPreference) findPreference(SyncthingService.PREF_SYNC_ONLY_WIFI); mSyncOnlyWifi = (CheckBoxPreference) findPreference(SyncthingService.PREF_SYNC_ONLY_WIFI);
Preference useRoot = findPreference(SyncthingService.PREF_USE_ROOT);
Preference appVersion = screen.findPreference(APP_VERSION_KEY); Preference appVersion = screen.findPreference(APP_VERSION_KEY);
mOptionsScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_OPTIONS_KEY); mOptionsScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_OPTIONS_KEY);
mGuiScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_GUI_KEY); mGuiScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_GUI_KEY);
@ -143,6 +146,10 @@ public class SettingsFragment extends PreferenceFragment
mAlwaysRunInBackground.setOnPreferenceChangeListener(this); mAlwaysRunInBackground.setOnPreferenceChangeListener(this);
mSyncOnlyCharging.setOnPreferenceChangeListener(this); mSyncOnlyCharging.setOnPreferenceChangeListener(this);
mSyncOnlyWifi.setOnPreferenceChangeListener(this); mSyncOnlyWifi.setOnPreferenceChangeListener(this);
if (!Shell.SU.available()) {
screen.removePreference(useRoot);
}
useRoot.setOnPreferenceChangeListener(this);
screen.findPreference(EXPORT_CONFIG).setOnPreferenceClickListener(this); screen.findPreference(EXPORT_CONFIG).setOnPreferenceClickListener(this);
screen.findPreference(IMPORT_CONFIG).setOnPreferenceClickListener(this); screen.findPreference(IMPORT_CONFIG).setOnPreferenceClickListener(this);
screen.findPreference(SYNCTHING_RESET).setOnPreferenceClickListener(this); screen.findPreference(SYNCTHING_RESET).setOnPreferenceClickListener(this);
@ -234,23 +241,25 @@ public class SettingsFragment extends PreferenceFragment
RestApi.TYPE_GUI, preference.getKey(), o, false, getActivity()); RestApi.TYPE_GUI, preference.getKey(), o, false, getActivity());
} }
boolean requireRestart = false;
// Avoid any code injection. // Avoid any code injection.
int error = 0; int error = 0;
if (preference.getKey().equals(STTRACE)) { if (preference.getKey().equals(STTRACE)) {
if (((String) o).matches("[a-z, ]*")) if (((String) o).matches("[a-z, ]*"))
mSyncthingService.getApi().requireRestart(getActivity()); requireRestart = true;
else else
error = R.string.toast_invalid_sttrace; error = R.string.toast_invalid_sttrace;
} else if (preference.getKey().equals(GUI_USER)) { } else if (preference.getKey().equals(GUI_USER)) {
String s = (String) o; String s = (String) o;
if (!s.contains(":") && !s.contains("'")) if (!s.contains(":") && !s.contains("'"))
mSyncthingService.getApi().requireRestart(getActivity()); requireRestart = true;
else else
error = R.string.toast_invalid_username; error = R.string.toast_invalid_username;
} else if (preference.getKey().equals(GUI_PASSWORD)) { } else if (preference.getKey().equals(GUI_PASSWORD)) {
String s = (String) o; String s = (String) o;
if (!s.contains(":") && !s.contains("'")) if (!s.contains(":") && !s.contains("'"))
mSyncthingService.getApi().requireRestart(getActivity()); requireRestart = true;
else else
error = R.string.toast_invalid_password; error = R.string.toast_invalid_password;
} }
@ -259,6 +268,13 @@ public class SettingsFragment extends PreferenceFragment
return false; return false;
} }
if (preference.getKey().equals(SyncthingService.PREF_USE_ROOT))
requireRestart = true;
if (requireRestart)
mSyncthingService.getApi().requireRestart(getActivity());
return true; return true;
} }

View File

@ -267,7 +267,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
} }
/** /**
* Stops syncthing. You should probably use SyncthingService.stopService() instead. * Stops syncthing and cancels notification. For use by {@link SyncthingService}.
*/ */
public void shutdown() { public void shutdown() {
// Happens in unit tests. // Happens in unit tests.
@ -277,7 +277,6 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
NotificationManager nm = (NotificationManager) NotificationManager nm = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE); mContext.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(NOTIFICATION_RESTART); nm.cancel(NOTIFICATION_RESTART);
SyncthingRunnable.killSyncthing();
mRestartPostponed = false; mRestartPostponed = false;
} }
@ -740,7 +739,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener,
case "": return c.getString(R.string.state_unknown); case "": return c.getString(R.string.state_unknown);
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
throw new AssertionError("Unexpected folder state"); throw new AssertionError("Unexpected folder state " + state);
} }
return ""; return "";
} }

View File

@ -5,6 +5,7 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Environment; import android.os.Environment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -15,6 +16,8 @@ import java.io.InputStreamReader;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import eu.chainfire.libsuperuser.Shell;
/** /**
* Runs the syncthing binary from command line, and prints its output to logcat. * Runs the syncthing binary from command line, and prints its output to logcat.
*/ */
@ -88,7 +91,6 @@ public class SyncthingRunnable implements Runnable {
try { try {
ProcessBuilder pb = new ProcessBuilder("chmod", "+x", mSyncthingBinary); ProcessBuilder pb = new ProcessBuilder("chmod", "+x", mSyncthingBinary);
Process p = pb.start(); Process p = pb.start();
if (p != null)
p.waitFor(); p.waitFor();
} catch (IOException|InterruptedException e) { } catch (IOException|InterruptedException e) {
Log.w(TAG, "Failed to chmod Syncthing", e); Log.w(TAG, "Failed to chmod Syncthing", e);
@ -98,7 +100,10 @@ public class SyncthingRunnable implements Runnable {
try { try {
// Loop to handle Syncthing restarts (these always have an error code of 3). // Loop to handle Syncthing restarts (these always have an error code of 3).
do { do {
ProcessBuilder pb = new ProcessBuilder(mCommand); ProcessBuilder pb = (useRoot())
? new ProcessBuilder("su", "-c", TextUtils.join(" ", mCommand))
: new ProcessBuilder(mCommand);
Map<String, String> env = pb.environment(); Map<String, String> env = pb.environment();
// Set home directory to data folder for web GUI folder picker. // Set home directory to data folder for web GUI folder picker.
env.put("HOME", Environment.getExternalStorageDirectory().getAbsolutePath()); env.put("HOME", Environment.getExternalStorageDirectory().getAbsolutePath());
@ -138,6 +143,14 @@ public class SyncthingRunnable implements Runnable {
} }
} }
/**
* 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();
}
/** /**
* Look for a running libsyncthing.so process and nice its IO. * Look for a running libsyncthing.so process and nice its IO.
*/ */
@ -149,7 +162,7 @@ public class SyncthingRunnable implements Runnable {
int ret = 1; int ret = 1;
try { try {
Thread.sleep(1000); // Wait a second before getting the pid Thread.sleep(1000); // Wait a second before getting the pid
nice = Runtime.getRuntime().exec("sh"); nice = Runtime.getRuntime().exec((useRoot()) ? "su" : "sh");
niceOut = new DataOutputStream(nice.getOutputStream()); niceOut = new DataOutputStream(nice.getOutputStream());
niceOut.writeBytes("set `ps | grep libsyncthing.so`\n"); niceOut.writeBytes("set `ps | grep libsyncthing.so`\n");
niceOut.writeBytes("ionice $2 be 7\n"); // best-effort, low priority niceOut.writeBytes("ionice $2 be 7\n"); // best-effort, low priority
@ -182,9 +195,8 @@ public class SyncthingRunnable implements Runnable {
/** /**
* Look for running libsyncthing.so processes and kill them. * Look for running libsyncthing.so processes and kill them.
* Try a SIGTERM once, then try again (twice) with SIGKILL. * Try a SIGTERM once, then try again (twice) with SIGKILL.
*
*/ */
public static void killSyncthing() { public void killSyncthing() {
final Process p = mSyncthing.get(); final Process p = mSyncthing.get();
if (p != null) { if (p != null) {
mSyncthing.set(null); mSyncthing.set(null);
@ -201,7 +213,7 @@ public class SyncthingRunnable implements Runnable {
Process ps = null; Process ps = null;
DataOutputStream psOut = null; DataOutputStream psOut = null;
try { try {
ps = Runtime.getRuntime().exec("sh"); ps = Runtime.getRuntime().exec((useRoot()) ? "su" : "sh");
psOut = new DataOutputStream(ps.getOutputStream()); psOut = new DataOutputStream(ps.getOutputStream());
psOut.writeBytes("ps | grep libsyncthing.so\n"); psOut.writeBytes("ps | grep libsyncthing.so\n");
psOut.writeBytes("exit\n"); psOut.writeBytes("exit\n");

View File

@ -92,6 +92,8 @@ public class SyncthingService extends Service {
public static final String PREF_SYNC_ONLY_CHARGING = "sync_only_charging"; public static final String PREF_SYNC_ONLY_CHARGING = "sync_only_charging";
public static final String PREF_USE_ROOT = "use_root";
private static final int NOTIFICATION_ACTIVE = 1; private static final int NOTIFICATION_ACTIVE = 1;
private ConfigXml mConfig; private ConfigXml mConfig;
@ -142,6 +144,8 @@ public class SyncthingService extends Service {
private DeviceStateHolder mDeviceStateHolder; private DeviceStateHolder mDeviceStateHolder;
private SyncthingRunnable mRunnable;
/** /**
* Handles intents, either {@link #ACTION_RESTART}, or intents having * Handles intents, either {@link #ACTION_RESTART}, or intents having
* {@link DeviceStateHolder#EXTRA_HAS_WIFI} or {@link DeviceStateHolder#EXTRA_IS_CHARGING} * {@link DeviceStateHolder#EXTRA_HAS_WIFI} or {@link DeviceStateHolder#EXTRA_IS_CHARGING}
@ -153,11 +157,11 @@ public class SyncthingService extends Service {
return START_STICKY; return START_STICKY;
if (ACTION_RESTART.equals(intent.getAction()) && mCurrentState == State.ACTIVE) { if (ACTION_RESTART.equals(intent.getAction()) && mCurrentState == State.ACTIVE) {
mApi.shutdown(); shutdown();
mCurrentState = State.INIT; mCurrentState = State.INIT;
updateState(); updateState();
} else if (ACTION_RESET.equals(intent.getAction())) { } else if (ACTION_RESET.equals(intent.getAction())) {
mApi.shutdown(); shutdown();
new SyncthingRunnable(this, SyncthingRunnable.Command.reset).run(); new SyncthingRunnable(this, SyncthingRunnable.Command.reset).run();
mCurrentState = State.INIT; mCurrentState = State.INIT;
updateState(); updateState();
@ -203,14 +207,15 @@ public class SyncthingService extends Service {
// HACK: Make sure there is no syncthing binary left running from an improper // HACK: Make sure there is no syncthing binary left running from an improper
// shutdown (eg Play Store update). // shutdown (eg Play Store update).
// NOTE: This will log an exception if syncthing is not actually running. // NOTE: This will log an exception if syncthing is not actually running.
mApi.shutdown(); shutdown();
Log.i(TAG, "Starting syncthing according to current state and preferences"); Log.i(TAG, "Starting syncthing according to current state and preferences");
mConfig = new ConfigXml(SyncthingService.this); mConfig = new ConfigXml(SyncthingService.this);
mCurrentState = State.STARTING; mCurrentState = State.STARTING;
registerOnWebGuiAvailableListener(mApi); registerOnWebGuiAvailableListener(mApi);
new PollWebGuiAvailableTaskImpl(getFilesDir() + "/" + HTTPS_CERT_FILE).execute(mConfig.getWebGuiUrl()); new PollWebGuiAvailableTaskImpl(getFilesDir() + "/" + HTTPS_CERT_FILE).execute(mConfig.getWebGuiUrl());
new Thread(new SyncthingRunnable(this, SyncthingRunnable.Command.main)).start(); mRunnable = new SyncthingRunnable(this, SyncthingRunnable.Command.main);
new Thread(mRunnable).start();
Notification n = new NotificationCompat.Builder(this) Notification n = new NotificationCompat.Builder(this)
.setContentTitle(getString(R.string.syncthing_active)) .setContentTitle(getString(R.string.syncthing_active))
.setSmallIcon(R.drawable.ic_stat_notify) .setSmallIcon(R.drawable.ic_stat_notify)
@ -229,14 +234,7 @@ public class SyncthingService extends Service {
Log.i(TAG, "Stopping syncthing according to current state and preferences"); Log.i(TAG, "Stopping syncthing according to current state and preferences");
mCurrentState = State.DISABLED; mCurrentState = State.DISABLED;
if (mApi != null) { shutdown();
mApi.shutdown();
for (FolderObserver ro : mObservers) {
ro.stopWatching();
}
mObservers.clear();
}
nm.cancel(NOTIFICATION_ACTIVE);
} }
onApiChange(); onApiChange();
} }
@ -331,11 +329,23 @@ public class SyncthingService extends Service {
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
Log.i(TAG, "Shutting down service"); Log.i(TAG, "Shutting down service");
if (mApi != null) { shutdown();
mApi.shutdown();
} }
private void shutdown() {
if (mRunnable != null)
mRunnable.killSyncthing();
if (mApi != null)
mApi.shutdown();
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancel(NOTIFICATION_ACTIVE); nm.cancel(NOTIFICATION_ACTIVE);
for (FolderObserver ro : mObservers) {
ro.stopWatching();
}
mObservers.clear();
} }
/** /**
@ -400,7 +410,7 @@ public class SyncthingService extends Service {
if (mStopScheduled) { if (mStopScheduled) {
mCurrentState = State.DISABLED; mCurrentState = State.DISABLED;
onApiChange(); onApiChange();
mApi.shutdown(); shutdown();
mStopScheduled = false; mStopScheduled = false;
return; return;
} }

View File

@ -25,6 +25,12 @@
android:summary="@string/advanced_folder_picker_summary" android:summary="@string/advanced_folder_picker_summary"
android:defaultValue="false" /> android:defaultValue="false" />
<CheckBoxPreference
android:key="use_root"
android:title="Sync as root"
android:summary="Run syncthing as superuser"
android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory