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 {
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'

View File

@ -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;
}

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() {
// 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 "";
}

View File

@ -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,7 +91,6 @@ public class SyncthingRunnable implements Runnable {
try {
ProcessBuilder pb = new ProcessBuilder("chmod", "+x", mSyncthingBinary);
Process p = pb.start();
if (p != null)
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<String, String> 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");

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_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) {
mApi.shutdown();
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;
}

View File

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