Implement native mode without binary copying
Plugins that support this mode only need to implement `NativePluginProvider.getExecutable`.
This commit is contained in:
@@ -159,7 +159,7 @@ trait BaseService extends Service {
|
||||
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES))
|
||||
|
||||
plugin = new PluginConfiguration(profile.plugin).selectedOptions
|
||||
pluginPath = PluginManager.initPlugin(plugin.id)
|
||||
pluginPath = PluginManager.init(plugin)
|
||||
}
|
||||
|
||||
def startRunner(profile: Profile) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import java.io.{File, FileNotFoundException, FileOutputStream, IOException}
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.{BroadcastReceiver, ContentResolver, Intent}
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.ShadowsocksApplication.app
|
||||
import com.github.shadowsocks.utils.CloseUtils.autoClose
|
||||
import com.github.shadowsocks.utils.{Commandline, IOUtils}
|
||||
@@ -41,11 +43,11 @@ object PluginManager {
|
||||
|
||||
// the following parts are meant to be used by :bg
|
||||
@throws[Throwable]
|
||||
def initPlugin(id: String): String = {
|
||||
if (id.isEmpty) return null
|
||||
def init(options: PluginOptions): String = {
|
||||
if (options.id.isEmpty) return null
|
||||
var throwable: Throwable = null
|
||||
|
||||
try initNativePlugin(id) match {
|
||||
try initNative(options) match {
|
||||
case null =>
|
||||
case path => return path
|
||||
} catch {
|
||||
@@ -56,44 +58,61 @@ object PluginManager {
|
||||
|
||||
// add other plugin types here
|
||||
|
||||
throw if (throwable == null)
|
||||
new FileNotFoundException(app.getString(com.github.shadowsocks.R.string.plugin_unknown, id)) else throwable
|
||||
throw if (throwable == null) new FileNotFoundException(
|
||||
app.getString(com.github.shadowsocks.R.string.plugin_unknown, options.id)) else throwable
|
||||
}
|
||||
|
||||
@throws[IOException]
|
||||
@throws[IndexOutOfBoundsException]
|
||||
@throws[AssertionError]
|
||||
private def initNativePlugin(id: String): String = {
|
||||
private def initNative(options: PluginOptions): String = {
|
||||
val providers = app.getPackageManager.queryIntentContentProviders(
|
||||
new Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(id)), 0)
|
||||
new Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
|
||||
assert(providers.length == 1)
|
||||
val builder = new Uri.Builder()
|
||||
val uri = new Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(providers(0).providerInfo.authority)
|
||||
.build()
|
||||
val cr = app.getContentResolver
|
||||
val cursor = cr.query(builder.build(), Array(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
|
||||
null, null, null)
|
||||
if (cursor != null) {
|
||||
var initialized = false
|
||||
val pluginDir = new File(app.getFilesDir, "plugin")
|
||||
def entryNotFound() = throw new IndexOutOfBoundsException("Plugin entry binary not found")
|
||||
if (!cursor.moveToFirst()) entryNotFound()
|
||||
IOUtils.deleteRecursively(pluginDir)
|
||||
if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory")
|
||||
val pluginDirPath = pluginDir.getAbsolutePath + '/'
|
||||
val list = new ListBuffer[String]
|
||||
do {
|
||||
val path = cursor.getString(0)
|
||||
val file = new File(pluginDir, path)
|
||||
assert(file.getAbsolutePath.startsWith(pluginDirPath))
|
||||
autoClose(cr.openInputStream(builder.path(path).build()))(in =>
|
||||
autoClose(new FileOutputStream(file))(out => IOUtils.copy(in, out)))
|
||||
list += Commandline.toString(Array("chmod", cursor.getString(1), file.getAbsolutePath))
|
||||
if (path == id) initialized = true
|
||||
} while (cursor.moveToNext())
|
||||
if (!initialized) entryNotFound()
|
||||
Shell.SH.run(list)
|
||||
new File(pluginDir, id).getAbsolutePath
|
||||
} else null
|
||||
try initNativeFast(cr, options, uri) catch {
|
||||
case t: Throwable =>
|
||||
t.printStackTrace()
|
||||
Log.w("PluginManager", "Initializing native plugin fast mode failed. Falling back to slow mode.")
|
||||
initNativeSlow(cr, options, uri)
|
||||
}
|
||||
}
|
||||
|
||||
private def initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String = {
|
||||
val out = new Bundle()
|
||||
out.putString(PluginContract.EXTRA_OPTIONS, options.id)
|
||||
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out).getString(PluginContract.EXTRA_ENTRY)
|
||||
assert(new File(result).canExecute)
|
||||
result
|
||||
}
|
||||
|
||||
private def initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String =
|
||||
cr.query(uri, Array(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null) match {
|
||||
case null => null
|
||||
case cursor =>
|
||||
var initialized = false
|
||||
val pluginDir = new File(app.getFilesDir, "plugin")
|
||||
def entryNotFound() = throw new IndexOutOfBoundsException("Plugin entry binary not found")
|
||||
if (!cursor.moveToFirst()) entryNotFound()
|
||||
IOUtils.deleteRecursively(pluginDir)
|
||||
if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory")
|
||||
val pluginDirPath = pluginDir.getAbsolutePath + '/'
|
||||
val list = new ListBuffer[String]
|
||||
do {
|
||||
val path = cursor.getString(0)
|
||||
val file = new File(pluginDir, path)
|
||||
assert(file.getAbsolutePath.startsWith(pluginDirPath))
|
||||
autoClose(cr.openInputStream(uri.buildUpon().path(path).build()))(in =>
|
||||
autoClose(new FileOutputStream(file))(out => IOUtils.copy(in, out)))
|
||||
list += Commandline.toString(Array("chmod", cursor.getString(1), file.getAbsolutePath))
|
||||
if (path == options.id) initialized = true
|
||||
} while (cursor.moveToNext())
|
||||
if (!initialized) entryNotFound()
|
||||
Shell.SH.run(list)
|
||||
new File(pluginDir, options.id).getAbsolutePath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ public final class PluginContract {
|
||||
*/
|
||||
public static final String ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP";
|
||||
|
||||
/**
|
||||
* The lookup key for a string that provides the plugin entry binary.
|
||||
*
|
||||
* Example: "/data/data/com.github.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
|
||||
*
|
||||
* Constant Value: "com.github.shadowsocks.plugin.EXTRA_ENTRY"
|
||||
*/
|
||||
public static final String EXTRA_ENTRY = "com.github.shadowsocks.plugin.EXTRA_ENTRY";
|
||||
/**
|
||||
* The lookup key for a string that provides the whole command as a string.
|
||||
*
|
||||
@@ -61,6 +69,8 @@ public final class PluginContract {
|
||||
*/
|
||||
public static final String METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config";
|
||||
|
||||
static final String METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable";
|
||||
|
||||
/**
|
||||
* Relative to the file to be copied. This column is required.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.github.shadowsocks.plugin
|
||||
import android.content.{ContentProvider, ContentValues}
|
||||
import android.database.{Cursor, MatrixCursor}
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.{Bundle, ParcelFileDescriptor}
|
||||
|
||||
/**
|
||||
* Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
|
||||
@@ -52,12 +52,30 @@ abstract class NativePluginProvider extends ContentProvider {
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns executable entry absolute path. This is used if plugin is sharing UID with the host.
|
||||
*
|
||||
* Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the default
|
||||
* behavior.
|
||||
*
|
||||
* @return Absolute path for executable entry.
|
||||
*/
|
||||
def getExecutable: String = throw new UnsupportedOperationException
|
||||
|
||||
def openFile(uri: Uri): ParcelFileDescriptor
|
||||
override def openFile(uri: Uri, mode: String): ParcelFileDescriptor = {
|
||||
assert(mode == "r")
|
||||
openFile(uri)
|
||||
}
|
||||
|
||||
override def call(method: String, arg: String, extras: Bundle): Bundle = method match {
|
||||
case PluginContract.METHOD_GET_EXECUTABLE =>
|
||||
val out = new Bundle()
|
||||
out.putString(PluginContract.EXTRA_ENTRY, getExecutable)
|
||||
out
|
||||
case _ => super.call(method, arg, extras)
|
||||
}
|
||||
|
||||
// Methods that should not be used
|
||||
override def update(uri: Uri, values: ContentValues, selection: String, selectionArgs: Array[String]): Int = ???
|
||||
override def insert(uri: Uri, values: ContentValues): Uri = ???
|
||||
|
||||
Reference in New Issue
Block a user