From 2fb425b441778fb05d7bbe91a1445f1615142204 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Sun, 17 May 2015 21:12:24 +0200 Subject: [PATCH] Added option to run syncthing as root (fixes #48). --- build.gradle | 8 ++-- .../fragments/SettingsFragment.java | 22 ++++++++-- .../syncthingandroid/syncthing/RestApi.java | 5 +-- .../syncthing/SyncthingRunnable.java | 26 ++++++++---- .../syncthing/SyncthingService.java | 40 ++++++++++++------- src/main/res/xml/app_settings.xml | 6 +++ 6 files changed, 76 insertions(+), 31 deletions(-) diff --git a/build.gradle b/build.gradle index 2f8397cf..b056c09f 100644 --- a/build.gradle +++ b/build.gradle @@ -19,13 +19,15 @@ apply plugin: 'com.github.ben-manes.versions' repositories { mavenCentral() maven { - url { - 'https://raw.github.com/kolavar/android-support-v4-preferencefragment/master/maven-repository/' - } + url 'https://raw.github.com/kolavar/android-support-v4-preferencefragment/master/maven-repository/' + } + maven { + url 'http://jcenter.bintray.com' } } dependencies { + compile 'eu.chainfire:libsuperuser:1.0.0.201504231659' compile 'com.android.support:appcompat-v7:22.0.0' compile 'com.android.support:support-v4-preferencefragment:1.0.0@aar' androidTestCompile 'com.squareup.okhttp:mockwebserver:2.3.0' diff --git a/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java b/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java index ae044799..c015a68d 100644 --- a/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java +++ b/src/main/java/com/nutomic/syncthingandroid/fragments/SettingsFragment.java @@ -25,6 +25,8 @@ import com.nutomic.syncthingandroid.activities.SyncthingActivity; import com.nutomic.syncthingandroid.syncthing.RestApi; import com.nutomic.syncthingandroid.syncthing.SyncthingService; +import eu.chainfire.libsuperuser.Shell; + public class SettingsFragment extends PreferenceFragment implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnApiChangeListener, Preference.OnPreferenceChangeListener, @@ -126,6 +128,7 @@ public class SettingsFragment extends PreferenceFragment mSyncOnlyCharging = (CheckBoxPreference) findPreference(SyncthingService.PREF_SYNC_ONLY_CHARGING); mSyncOnlyWifi = (CheckBoxPreference) findPreference(SyncthingService.PREF_SYNC_ONLY_WIFI); + Preference useRoot = findPreference(SyncthingService.PREF_USE_ROOT); Preference appVersion = screen.findPreference(APP_VERSION_KEY); mOptionsScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_OPTIONS_KEY); mGuiScreen = (PreferenceScreen) screen.findPreference(SYNCTHING_GUI_KEY); @@ -143,6 +146,10 @@ public class SettingsFragment extends PreferenceFragment mAlwaysRunInBackground.setOnPreferenceChangeListener(this); mSyncOnlyCharging.setOnPreferenceChangeListener(this); mSyncOnlyWifi.setOnPreferenceChangeListener(this); + if (!Shell.SU.available()) { + screen.removePreference(useRoot); + } + useRoot.setOnPreferenceChangeListener(this); screen.findPreference(EXPORT_CONFIG).setOnPreferenceClickListener(this); screen.findPreference(IMPORT_CONFIG).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()); } + boolean requireRestart = false; + // Avoid any code injection. int error = 0; if (preference.getKey().equals(STTRACE)) { if (((String) o).matches("[a-z, ]*")) - mSyncthingService.getApi().requireRestart(getActivity()); + requireRestart = true; else error = R.string.toast_invalid_sttrace; } else if (preference.getKey().equals(GUI_USER)) { String s = (String) o; if (!s.contains(":") && !s.contains("'")) - mSyncthingService.getApi().requireRestart(getActivity()); + requireRestart = true; else error = R.string.toast_invalid_username; } else if (preference.getKey().equals(GUI_PASSWORD)) { String s = (String) o; if (!s.contains(":") && !s.contains("'")) - mSyncthingService.getApi().requireRestart(getActivity()); + requireRestart = true; else error = R.string.toast_invalid_password; } @@ -259,6 +268,13 @@ public class SettingsFragment extends PreferenceFragment return false; } + if (preference.getKey().equals(SyncthingService.PREF_USE_ROOT)) + requireRestart = true; + + if (requireRestart) + mSyncthingService.getApi().requireRestart(getActivity()); + + return true; } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index c998182d..0653c9c7 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -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() { // Happens in unit tests. @@ -277,7 +277,6 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, NotificationManager nm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(NOTIFICATION_RESTART); - SyncthingRunnable.killSyncthing(); mRestartPostponed = false; } @@ -740,7 +739,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, case "": return c.getString(R.string.state_unknown); } if (BuildConfig.DEBUG) { - throw new AssertionError("Unexpected folder state"); + throw new AssertionError("Unexpected folder state " + state); } return ""; } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingRunnable.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingRunnable.java index 957b2821..7dfbcfa1 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingRunnable.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingRunnable.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Environment; import android.preference.PreferenceManager; +import android.text.TextUtils; import android.util.Log; import java.io.BufferedReader; @@ -15,6 +16,8 @@ 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. */ @@ -88,8 +91,7 @@ public class SyncthingRunnable implements Runnable { try { ProcessBuilder pb = new ProcessBuilder("chmod", "+x", mSyncthingBinary); Process p = pb.start(); - if (p != null) - p.waitFor(); + p.waitFor(); } catch (IOException|InterruptedException e) { Log.w(TAG, "Failed to chmod Syncthing", e); } @@ -98,7 +100,10 @@ public class SyncthingRunnable implements Runnable { try { // Loop to handle Syncthing restarts (these always have an error code of 3). do { - ProcessBuilder pb = new ProcessBuilder(mCommand); + ProcessBuilder pb = (useRoot()) + ? new ProcessBuilder("su", "-c", TextUtils.join(" ", mCommand)) + : new ProcessBuilder(mCommand); + Map env = pb.environment(); // Set home directory to data folder for web GUI folder picker. 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. */ @@ -149,7 +162,7 @@ public class SyncthingRunnable implements Runnable { int ret = 1; try { 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.writeBytes("set `ps | grep libsyncthing.so`\n"); 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. * Try a SIGTERM once, then try again (twice) with SIGKILL. - * */ - public static void killSyncthing() { + public void killSyncthing() { final Process p = mSyncthing.get(); if (p != null) { mSyncthing.set(null); @@ -201,7 +213,7 @@ public class SyncthingRunnable implements Runnable { Process ps = null; DataOutputStream psOut = null; try { - ps = Runtime.getRuntime().exec("sh"); + ps = Runtime.getRuntime().exec((useRoot()) ? "su" : "sh"); psOut = new DataOutputStream(ps.getOutputStream()); psOut.writeBytes("ps | grep libsyncthing.so\n"); psOut.writeBytes("exit\n"); diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java index 8ed220c8..f5a6ea2a 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java @@ -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_USE_ROOT = "use_root"; + private static final int NOTIFICATION_ACTIVE = 1; private ConfigXml mConfig; @@ -142,6 +144,8 @@ public class SyncthingService extends Service { private DeviceStateHolder mDeviceStateHolder; + private SyncthingRunnable mRunnable; + /** * Handles intents, either {@link #ACTION_RESTART}, or intents having * {@link DeviceStateHolder#EXTRA_HAS_WIFI} or {@link DeviceStateHolder#EXTRA_IS_CHARGING} @@ -153,11 +157,11 @@ public class SyncthingService extends Service { return START_STICKY; if (ACTION_RESTART.equals(intent.getAction()) && mCurrentState == State.ACTIVE) { - mApi.shutdown(); + shutdown(); mCurrentState = State.INIT; updateState(); } else if (ACTION_RESET.equals(intent.getAction())) { - mApi.shutdown(); + shutdown(); new SyncthingRunnable(this, SyncthingRunnable.Command.reset).run(); mCurrentState = State.INIT; updateState(); @@ -203,14 +207,15 @@ public class SyncthingService extends Service { // HACK: Make sure there is no syncthing binary left running from an improper // shutdown (eg Play Store update). // 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"); mConfig = new ConfigXml(SyncthingService.this); mCurrentState = State.STARTING; registerOnWebGuiAvailableListener(mApi); 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) .setContentTitle(getString(R.string.syncthing_active)) .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"); mCurrentState = State.DISABLED; - if (mApi != null) { - mApi.shutdown(); - for (FolderObserver ro : mObservers) { - ro.stopWatching(); - } - mObservers.clear(); - } - nm.cancel(NOTIFICATION_ACTIVE); + shutdown(); } onApiChange(); } @@ -331,11 +329,23 @@ public class SyncthingService extends Service { public void onDestroy() { super.onDestroy(); Log.i(TAG, "Shutting down service"); - if (mApi != null) { + shutdown(); + } + + private void shutdown() { + if (mRunnable != null) + mRunnable.killSyncthing(); + + if (mApi != null) mApi.shutdown(); - } + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); nm.cancel(NOTIFICATION_ACTIVE); + + for (FolderObserver ro : mObservers) { + ro.stopWatching(); + } + mObservers.clear(); } /** @@ -400,7 +410,7 @@ public class SyncthingService extends Service { if (mStopScheduled) { mCurrentState = State.DISABLED; onApiChange(); - mApi.shutdown(); + shutdown(); mStopScheduled = false; return; } diff --git a/src/main/res/xml/app_settings.xml b/src/main/res/xml/app_settings.xml index fe49cbde..e76f31b3 100644 --- a/src/main/res/xml/app_settings.xml +++ b/src/main/res/xml/app_settings.xml @@ -25,6 +25,12 @@ android:summary="@string/advanced_folder_picker_summary" android:defaultValue="false" /> + +