From d5311120757a21db572f52c596f47620c53ff4b4 Mon Sep 17 00:00:00 2001 From: John Zhao Date: Wed, 16 Jan 2019 10:41:12 +0800 Subject: [PATCH] feat(android): add libshell and list devices --- wrappers/android/mynteye/app/build.gradle | 1 + .../mynteye/app/src/main/AndroidManifest.xml | 6 +- .../slightech/mynteye/demo/MainActivity.java | 20 - .../mynteye/demo/ui/BaseActivity.java | 63 + .../mynteye/demo/ui/MainActivity.java | 105 ++ .../mynteye/demo/util/RootUtils.java | 76 ++ .../app/src/main/res/layout/activity_main.xml | 3 +- .../app/src/main/res/menu/menu_main.xml | 9 + .../app/src/main/res/values/strings.xml | 2 + .../src/main/cpp/mynteye/impl/device_impl.cpp | 9 + wrappers/android/mynteye/libshell/.gitignore | 1 + wrappers/android/mynteye/libshell/README.md | 2 + .../android/mynteye/libshell/build.gradle | 29 + .../mynteye/libshell/proguard-rules.pro | 21 + .../libshell/src/main/AndroidManifest.xml | 2 + .../com/stericson/RootShell/RootShell.java | 610 ++++++++++ .../RootShell/containers/RootClass.java | 348 ++++++ .../exceptions/RootDeniedException.java | 32 + .../RootShell/execution/Command.java | 350 ++++++ .../RootShell/execution/JavaCommand.java | 58 + .../stericson/RootShell/execution/Shell.java | 1038 +++++++++++++++++ wrappers/android/mynteye/settings.gradle | 2 +- 22 files changed, 2764 insertions(+), 23 deletions(-) delete mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java create mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/BaseActivity.java create mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java create mode 100644 wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/util/RootUtils.java create mode 100644 wrappers/android/mynteye/app/src/main/res/menu/menu_main.xml create mode 100644 wrappers/android/mynteye/libshell/.gitignore create mode 100644 wrappers/android/mynteye/libshell/README.md create mode 100644 wrappers/android/mynteye/libshell/build.gradle create mode 100644 wrappers/android/mynteye/libshell/proguard-rules.pro create mode 100644 wrappers/android/mynteye/libshell/src/main/AndroidManifest.xml create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/RootShell.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/containers/RootClass.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/exceptions/RootDeniedException.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Command.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/JavaCommand.java create mode 100644 wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Shell.java diff --git a/wrappers/android/mynteye/app/build.gradle b/wrappers/android/mynteye/app/build.gradle index 3e74552..5ba3d55 100644 --- a/wrappers/android/mynteye/app/build.gradle +++ b/wrappers/android/mynteye/app/build.gradle @@ -37,6 +37,7 @@ dependencies { annotationProcessor 'com.jakewharton:butterknife-compiler:10.0.0' implementation project(':libmynteye') + implementation project(':libshell') testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' diff --git a/wrappers/android/mynteye/app/src/main/AndroidManifest.xml b/wrappers/android/mynteye/app/src/main/AndroidManifest.xml index 0e03278..3633dae 100644 --- a/wrappers/android/mynteye/app/src/main/AndroidManifest.xml +++ b/wrappers/android/mynteye/app/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" package="com.slightech.mynteye.demo"> + + + + - + diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java deleted file mode 100644 index ef9edd7..0000000 --- a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/MainActivity.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.slightech.mynteye.demo; - -import androidx.appcompat.app.AppCompatActivity; -import android.os.Bundle; -import com.slightech.mynteye.Device; -import com.slightech.mynteye.DeviceUsbInfo; -import timber.log.Timber; - -public class MainActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - for (DeviceUsbInfo info : Device.query()) { - Timber.i(info.toString()); - } - } -} diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/BaseActivity.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/BaseActivity.java new file mode 100644 index 0000000..aa35d74 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/BaseActivity.java @@ -0,0 +1,63 @@ +package com.slightech.mynteye.demo.ui; + +import android.annotation.SuppressLint; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; + +@SuppressLint("Registered") +public class BaseActivity extends AppCompatActivity { + + private final int REQ_PERMISSIONS = 1; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestPermissions(); + } + + private void requestPermissions() { + final String[] permissions = new String[]{WRITE_EXTERNAL_STORAGE, CAMERA}; + + boolean granted = true; + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(this, permission) + != PackageManager.PERMISSION_GRANTED) { + granted = false; + } + } + if (granted) return; + + ActivityCompat.requestPermissions(this, permissions, REQ_PERMISSIONS); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (requestCode == REQ_PERMISSIONS) { + boolean granted = true; + if (grantResults.length < 1) { + granted = false; + } else { + for (int result : grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + granted = false; + } + } + } + if (!granted) { + Toast.makeText(this, "Permission denied :(", Toast.LENGTH_LONG).show(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } +} diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java new file mode 100644 index 0000000..23abf24 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/ui/MainActivity.java @@ -0,0 +1,105 @@ +package com.slightech.mynteye.demo.ui; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; +import butterknife.ButterKnife; +import com.slightech.mynteye.Device; +import com.slightech.mynteye.DeviceUsbInfo; +import com.slightech.mynteye.demo.R; +import com.slightech.mynteye.demo.util.RootUtils; +import java.util.ArrayList; +import java.util.Locale; + +public class MainActivity extends BaseActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + ButterKnife.bind(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + MenuItem item = menu.findItem(R.id.action_open); + if (item != null) { + item.setEnabled(false); + actionOpen(() -> item.setEnabled(true)); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_open: + item.setEnabled(false); + actionOpen(() -> item.setEnabled(true)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void actionOpen(final Runnable completeEvent) { + if (!RootUtils.isRooted()) { + alert("Warning", "Root denied :("); + return; + } + RootUtils.requestAccessible(ok -> { + if (completeEvent != null) completeEvent.run(); + if (ok) { + toast("Root granted :)"); + showDevices(); + } else { + alert("Warning", "There are no devices accessible."); + } + }); + } + + private void showDevices() { + ArrayList infos = Device.query(); + if (infos.isEmpty()) { + alert("Warning", "There are no devices :("); + } else { + ArrayList items = new ArrayList<>(); + for (DeviceUsbInfo info : infos) { + items.add(String.format(Locale.getDefault(), "%d, %s, SN: %s", + info.getIndex(), info.getName(), info.getSn())); + } + + AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle("Devices") + .create(); + ListView listView = new ListView(this); + listView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items)); + listView.setOnItemClickListener((parent, view, position, id) -> { + dialog.dismiss(); + openDevice(infos.get(position)); + }); + dialog.setView(listView); + dialog.show(); + } + } + + private void openDevice(DeviceUsbInfo info) { + } + + private void toast(CharSequence text) { + Toast.makeText(this, text, Toast.LENGTH_LONG).show(); + } + + private void alert(CharSequence title, CharSequence message) { + new AlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } +} diff --git a/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/util/RootUtils.java b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/util/RootUtils.java new file mode 100644 index 0000000..36c19a4 --- /dev/null +++ b/wrappers/android/mynteye/app/src/main/java/com/slightech/mynteye/demo/util/RootUtils.java @@ -0,0 +1,76 @@ +package com.slightech.mynteye.demo.util; + +import com.stericson.RootShell.RootShell; +import com.stericson.RootShell.exceptions.RootDeniedException; +import com.stericson.RootShell.execution.Command; +import com.stericson.RootShell.execution.Shell; +import java.io.IOException; +import java.util.concurrent.TimeoutException; +import timber.log.Timber; + +public final class RootUtils { + + public interface OnRequestAccessibleListener { + void onRequestAccessible(boolean ok); + } + + public static boolean isRooted() { + if (!RootShell.isRootAvailable()) { + Timber.e("Root not found"); + return false; + } + + try { + RootShell.getShell(true); + } catch (IOException e) { + e.printStackTrace(); + return false; + } catch (TimeoutException e) { + Timber.e("TIMEOUT EXCEPTION!"); + e.printStackTrace(); + return false; + } catch (RootDeniedException e) { + Timber.e("ROOT DENIED EXCEPTION!"); + e.printStackTrace(); + return false; + } + + try { + if (!RootShell.isAccessGiven()) { + Timber.e("ERROR: No root access to this device."); + return false; + } + } catch (Exception e) { + Timber.e("ERROR: could not determine root access to this device."); + return false; + } + + return true; + } + + public static void requestAccessible(OnRequestAccessibleListener l) { + try { + Shell sh = RootShell.getShell(true); + sh.add(new Command(1, "chmod 666 /dev/video*") { + @Override + public void commandOutput(int id, String line) { + Timber.d("commandOutput: %s", line); + super.commandOutput(id, line); + } + @Override + public void commandTerminated(int id, String reason) { + Timber.d("commandTerminated: %s", reason); + } + @Override + public void commandCompleted(int id, int exitcode) { + Timber.d("commandCompleted: %s", ((exitcode == 0) ? "ok" : "fail")); + if (l != null) l.onRequestAccessible(exitcode == 0); + } + }); + sh.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml b/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml index 8a0f7e4..32dae9a 100644 --- a/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml +++ b/wrappers/android/mynteye/app/src/main/res/layout/activity_main.xml @@ -5,10 +5,11 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity" + tools:context=".ui.MainActivity" > + + + diff --git a/wrappers/android/mynteye/app/src/main/res/values/strings.xml b/wrappers/android/mynteye/app/src/main/res/values/strings.xml index d69f619..4cb552d 100644 --- a/wrappers/android/mynteye/app/src/main/res/values/strings.xml +++ b/wrappers/android/mynteye/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ mynteye + + Open diff --git a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp index f9cb061..8677b10 100644 --- a/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp +++ b/wrappers/android/mynteye/libmynteye/src/main/cpp/mynteye/impl/device_impl.cpp @@ -2,6 +2,7 @@ #include "mynteye/device/context.h" #include "mynteye/device/device.h" +#include "mynteye/logger.h" #include "device_usb_info.hpp" #include "stream_request.hpp" @@ -12,6 +13,7 @@ MYNTEYE_USE_NAMESPACE namespace mynteye_jni { std::vector Device::Query() { + VLOG(2) << __func__; std::vector infos; Context context; @@ -27,6 +29,7 @@ std::vector Device::Query() { } std::shared_ptr Device::Create(const DeviceUsbInfo & info) { + VLOG(2) << __func__; Context context; int32_t i = 0; for (auto&& d : context.devices()) { @@ -39,12 +42,15 @@ std::shared_ptr Device::Create(const DeviceUsbInfo & info) { } DeviceImpl::DeviceImpl(const device_t & device) : Device(), device_(device) { + VLOG(2) << __func__; } DeviceImpl::~DeviceImpl() { + VLOG(2) << __func__; } std::vector DeviceImpl::GetStreamRequests() { + VLOG(2) << __func__; std::vector requests; int32_t i = 0; @@ -58,6 +64,7 @@ std::vector DeviceImpl::GetStreamRequests() { } void DeviceImpl::ConfigStreamRequest(const StreamRequest & request) { + VLOG(2) << __func__; int32_t i = 0; for (auto&& req : device_->GetStreamRequests()) { if (i == request.index) { @@ -69,9 +76,11 @@ void DeviceImpl::ConfigStreamRequest(const StreamRequest & request) { } void DeviceImpl::Start() { + VLOG(2) << __func__; } void DeviceImpl::Stop() { + VLOG(2) << __func__; } } // namespace mynteye_jni diff --git a/wrappers/android/mynteye/libshell/.gitignore b/wrappers/android/mynteye/libshell/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/wrappers/android/mynteye/libshell/.gitignore @@ -0,0 +1 @@ +/build diff --git a/wrappers/android/mynteye/libshell/README.md b/wrappers/android/mynteye/libshell/README.md new file mode 100644 index 0000000..d9948a7 --- /dev/null +++ b/wrappers/android/mynteye/libshell/README.md @@ -0,0 +1,2 @@ + +* [RootShell](https://github.com/Stericson/RootShell) diff --git a/wrappers/android/mynteye/libshell/build.gradle b/wrappers/android/mynteye/libshell/build.gradle new file mode 100644 index 0000000..f996e9a --- /dev/null +++ b/wrappers/android/mynteye/libshell/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion xversions.compileSdk + + defaultConfig { + minSdkVersion xversions.minSdk + targetSdkVersion xversions.targetSdk + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/wrappers/android/mynteye/libshell/proguard-rules.pro b/wrappers/android/mynteye/libshell/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/wrappers/android/mynteye/libshell/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/wrappers/android/mynteye/libshell/src/main/AndroidManifest.xml b/wrappers/android/mynteye/libshell/src/main/AndroidManifest.xml new file mode 100644 index 0000000..11442a8 --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/RootShell.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/RootShell.java new file mode 100644 index 0000000..d959e88 --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/RootShell.java @@ -0,0 +1,610 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ +package com.stericson.RootShell; + + +import com.stericson.RootShell.exceptions.RootDeniedException; +import com.stericson.RootShell.execution.Command; +import com.stericson.RootShell.execution.Shell; + +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeoutException; + +public class RootShell { + + // -------------------- + // # Public Variables # + // -------------------- + + public static boolean debugMode = false; + + public static final String version = "RootShell v1.6"; + + /** + * Setting this to false will disable the handler that is used + * by default for the 3 callback methods for Command. + *

+ * By disabling this all callbacks will be called from a thread other than + * the main UI thread. + */ + public static boolean handlerEnabled = true; + + + /** + * Setting this will change the default command timeout. + *

+ * The default is 20000ms + */ + public static int defaultCommandTimeout = 20000; + + public static enum LogLevel { + VERBOSE, + ERROR, + DEBUG, + WARN + } + // -------------------- + // # Public Methods # + // -------------------- + + /** + * This will close all open shells. + */ + public static void closeAllShells() throws IOException { + Shell.closeAll(); + } + + /** + * This will close the custom shell that you opened. + */ + public static void closeCustomShell() throws IOException { + Shell.closeCustomShell(); + } + + /** + * This will close either the root shell or the standard shell depending on what you specify. + * + * @param root a boolean to specify whether to close the root shell or the standard shell. + */ + public static void closeShell(boolean root) throws IOException { + if (root) { + Shell.closeRootShell(); + } else { + Shell.closeShell(); + } + } + + /** + * Use this to check whether or not a file exists on the filesystem. + * + * @param file String that represent the file, including the full path to the + * file and its name. + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file) { + return exists(file, false); + } + + /** + * Use this to check whether or not a file OR directory exists on the filesystem. + * + * @param file String that represent the file OR the directory, including the full path to the + * file and its name. + * @param isDir boolean that represent whether or not we are looking for a directory + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file, boolean isDir) { + final List result = new ArrayList(); + + String cmdToExecute = "ls " + (isDir ? "-d " : " "); + + Command command = new Command(0, false, cmdToExecute + file) { + @Override + public void commandOutput(int id, String line) { + RootShell.log(line); + result.add(line); + + super.commandOutput(id, line); + } + }; + + try { + //Try without root... + RootShell.getShell(false).add(command); + commandWait(RootShell.getShell(false), command); + + } catch (Exception e) { + RootShell.log("Exception: " + e); + return false; + } + + for (String line : result) { + if (line.trim().equals(file)) { + return true; + } + } + + result.clear(); + + command = new Command(0, false, cmdToExecute + file) { + @Override + public void commandOutput(int id, String line) { + RootShell.log(line); + result.add(line); + + super.commandOutput(id, line); + } + }; + + try { + RootShell.getShell(true).add(command); + commandWait(RootShell.getShell(true), command); + + } catch (Exception e) { + RootShell.log("Exception: " + e); + return false; + } + + //Avoid concurrent modification... + List final_result = new ArrayList(); + final_result.addAll(result); + + for (String line : final_result) { + if (line.trim().equals(file)) { + return true; + } + } + + return false; + + } + + /** + * @param binaryName String that represent the binary to find. + * @param singlePath boolean that represents whether to return a single path or multiple. + * + * @return List containing the locations the binary was found at. + */ + public static List findBinary(String binaryName, boolean singlePath) { + return findBinary(binaryName, null, singlePath); + } + + /** + * @param binaryName String that represent the binary to find. + * @param searchPaths List which contains the paths to search for this binary in. + * @param singlePath boolean that represents whether to return a single path or multiple. + * + * @return List containing the locations the binary was found at. + */ + public static List findBinary(final String binaryName, List searchPaths, boolean singlePath) { + + final List foundPaths = new ArrayList(); + + boolean found = false; + + if(searchPaths == null) + { + searchPaths = RootShell.getPath(); + } + + RootShell.log("Checking for " + binaryName); + + //Try to use stat first + try { + for (String path : searchPaths) { + + if(!path.endsWith("/")) + { + path += "/"; + } + + final String currentPath = path; + + Command cc = new Command(0, false, "stat " + path + binaryName) { + @Override + public void commandOutput(int id, String line) { + if (line.contains("File: ") && line.contains(binaryName)) { + foundPaths.add(currentPath); + + RootShell.log(binaryName + " was found here: " + currentPath); + } + + RootShell.log(line); + + super.commandOutput(id, line); + } + }; + + cc = RootShell.getShell(false).add(cc); + commandWait(RootShell.getShell(false), cc); + + if(foundPaths.size() > 0 && singlePath) { + break; + } + } + + found = !foundPaths.isEmpty(); + + } catch (Exception e) { + RootShell.log(binaryName + " was not found, more information MAY be available with Debugging on."); + } + + if (!found) { + RootShell.log("Trying second method"); + + for (String path : searchPaths) { + + if(!path.endsWith("/")) + { + path += "/"; + } + + if (RootShell.exists(path + binaryName)) { + RootShell.log(binaryName + " was found here: " + path); + foundPaths.add(path); + + if(foundPaths.size() > 0 && singlePath) { + break; + } + + } else { + RootShell.log(binaryName + " was NOT found here: " + path); + } + } + } + + Collections.reverse(foundPaths); + + return foundPaths; + } + + /** + * This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param shellPath a String to Indicate the path to the shell that you want to open. + * @param timeout an int to Indicate the length of time before giving up on opening a shell. + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException + { + return Shell.startCustomShell(shellPath, timeout); + } + + /** + * This will return the environment variable PATH + * + * @return List A List of Strings representing the environment variable $PATH + */ + public static List getPath() { + return Arrays.asList(System.getenv("PATH").split(":")); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + * @param retry a int to indicate how many times the ROOT shell should try to open with root priviliges... + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { + if (root) { + return Shell.startRootShell(timeout, shellContext, retry); + } else { + return Shell.startShell(timeout); + } + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param shellContext the context to execute the shell with + */ + public static Shell getShell(boolean root, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, 0, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + */ + public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, Shell.defaultContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + */ + public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException { + return RootShell.getShell(root, 0); + } + + /** + * @return true if your app has been given root access. + * @throws TimeoutException if this operation times out. (cannot determine if access is given) + */ + public static boolean isAccessGiven() { + return isAccessGiven(0, 3); + } + + /** + * Control how many time of retries should request + * + * @param timeout The timeout + * @param retries The number of retries + * + * @return true if your app has been given root access. + * @throws TimeoutException if this operation times out. (cannot determine if access is given) + */ + public static boolean isAccessGiven(int timeout, int retries) { + final Set ID = new HashSet(); + final int IAG = 158; + + try { + RootShell.log("Checking for Root access"); + + Command command = new Command(IAG, false, "id") { + @Override + public void commandOutput(int id, String line) { + if (id == IAG) { + ID.addAll(Arrays.asList(line.split(" "))); + } + + super.commandOutput(id, line); + } + }; + + Shell shell = Shell.startRootShell(timeout, retries); + shell.add(command); + commandWait(shell, command); + + //parse the userid + for (String userid : ID) { + RootShell.log(userid); + + if (userid.toLowerCase().contains("uid=0")) { + RootShell.log("Access Given"); + return true; + } + } + + return false; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * @return true if BusyBox was found. + */ + public static boolean isBusyboxAvailable() + { + return isBusyboxAvailable(false); + } + + /** + * @return true if BusyBox or Toybox was found. + */ + public static boolean isBusyboxAvailable(boolean includeToybox) + { + if(includeToybox) { + return (findBinary("busybox", true)).size() > 0 || (findBinary("toybox", true)).size() > 0; + } else { + return (findBinary("busybox", true)).size() > 0; + } + } + + /** + * @return true if su was found. + */ + public static boolean isRootAvailable() { + return (findBinary("su", true)).size() > 0; + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + */ + public static void log(String msg) { + log(null, msg, LogLevel.DEBUG, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + */ + public static void log(String TAG, String msg) { + log(TAG, msg, LogLevel.DEBUG, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug, 4 for warn + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String msg, LogLevel type, Exception e) { + log(null, msg, type, e); + } + + /** + * This method allows you to check whether logging is enabled. + * Yes, it has a goofy name, but that's to keep it as short as possible. + * After all writing logging calls should be painless. + * This method exists to save Android going through the various Java layers + * that are traversed any time a string is created (i.e. what you are logging) + *

+ * Example usage: + * if(islog) { + * StrinbBuilder sb = new StringBuilder(); + * // ... + * // build string + * // ... + * log(sb.toString()); + * } + * + * @return true if logging is enabled + */ + public static boolean islog() { + return debugMode; + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String TAG, String msg, LogLevel type, Exception e) { + if (msg != null && !msg.equals("")) { + if (debugMode) { + if (TAG == null) { + TAG = version; + } + + switch (type) { + case VERBOSE: + Log.v(TAG, msg); + break; + case ERROR: + Log.e(TAG, msg, e); + break; + case DEBUG: + Log.d(TAG, msg); + break; + case WARN: + Log.w(TAG, msg); + break; + } + } + } + } + + // -------------------- + // # Public Methods # + // -------------------- + + private static void commandWait(Shell shell, Command cmd) throws Exception { + while (!cmd.isFinished()) { + + RootShell.log(version, shell.getCommandQueuePositionString(cmd)); + RootShell.log(version, "Processed " + cmd.totalOutputProcessed + " of " + cmd.totalOutput + " output from command."); + + synchronized (cmd) { + try { + if (!cmd.isFinished()) { + cmd.wait(2000); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + if (!cmd.isExecuting() && !cmd.isFinished()) { + if (!shell.isExecuting && !shell.isReading) { + RootShell.log(version, "Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else if (shell.isExecuting && !shell.isReading) { + RootShell.log(version, "Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else { + RootShell.log(version, "Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } + } + + } + } +} diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/containers/RootClass.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/containers/RootClass.java new file mode 100644 index 0000000..9cf7659 --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/containers/RootClass.java @@ -0,0 +1,348 @@ +package com.stericson.RootShell.containers; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FilenameFilter; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* #ANNOTATIONS @SupportedAnnotationTypes("com.stericson.RootShell.containers.RootClass.Candidate") */ +/* #ANNOTATIONS @SupportedSourceVersion(SourceVersion.RELEASE_6) */ +public class RootClass /* #ANNOTATIONS extends AbstractProcessor */ { + + /* #ANNOTATIONS + @Override + public boolean process(Set typeElements, RoundEnvironment roundEnvironment) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "I was invoked!!!"); + + return false; + } + */ + + static String PATH_TO_DX = "/Users/Chris/Projects/android-sdk-macosx/build-tools/18.0.1/dx"; + + enum READ_STATE { + STARTING, FOUND_ANNOTATION; + } + + public RootClass(String[] args) throws ClassNotFoundException, NoSuchMethodException, + IllegalAccessException, InvocationTargetException, InstantiationException { + + // Note: rather than calling System.load("/system/lib/libandroid_runtime.so"); + // which would leave a bunch of unresolved JNI references, + // we are using the 'withFramework' class as a preloader. + // So, yeah, russian dolls: withFramework > RootClass > actual method + + String className = args[0]; + RootArgs actualArgs = new RootArgs(); + actualArgs.args = new String[args.length - 1]; + System.arraycopy(args, 1, actualArgs.args, 0, args.length - 1); + Class classHandler = Class.forName(className); + Constructor classConstructor = classHandler.getConstructor(RootArgs.class); + classConstructor.newInstance(actualArgs); + } + + public @interface Candidate { + + } + + ; + + public class RootArgs { + + public String args[]; + } + + static void displayError(Exception e) { + // Not using system.err to make it easier to capture from + // calling library. + System.out.println("##ERR##" + e.getMessage() + "##"); + e.printStackTrace(); + } + + // I reckon it would be better to investigate classes using getAttribute() + // however this method allows the developer to simply select "Run" on RootClass + // and immediately re-generate the necessary jar file. + static public class AnnotationsFinder { + + private final String AVOIDDIRPATH = "stericson" + File.separator + "RootShell" + File.separator; + + private List classFiles; + + public AnnotationsFinder() throws IOException { + System.out.println("Discovering root class annotations..."); + classFiles = new ArrayList(); + lookup(new File("src"), classFiles); + System.out.println("Done discovering annotations. Building jar file."); + File builtPath = getBuiltPath(); + if (null != builtPath) { + // Android! Y U no have com.google.common.base.Joiner class? + String rc1 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass.class"; + String rc2 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$RootArgs.class"; + String rc3 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder.class"; + String rc4 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder$1.class"; + String rc5 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder$2.class"; + String[] cmd; + boolean onWindows = (-1 != System.getProperty("os.name").toLowerCase().indexOf("win")); + if (onWindows) { + StringBuilder sb = new StringBuilder( + " " + rc1 + " " + rc2 + " " + rc3 + " " + rc4 + " " + rc5 + ); + for (File file : classFiles) { + sb.append(" " + file.getPath()); + } + cmd = new String[]{ + "cmd", "/C", + "jar cvf" + + " anbuild.jar" + + sb.toString() + }; + } else { + ArrayList al = new ArrayList(); + al.add("jar"); + al.add("cf"); + al.add("anbuild.jar"); + al.add(rc1); + al.add(rc2); + al.add(rc3); + al.add(rc4); + al.add(rc5); + for (File file : classFiles) { + al.add(file.getPath()); + } + cmd = al.toArray(new String[al.size()]); + } + ProcessBuilder jarBuilder = new ProcessBuilder(cmd); + jarBuilder.directory(builtPath); + try { + jarBuilder.start().waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + } + + String strRawFolder = "res" + File.separator + "raw"; + if (builtPath.toString().startsWith("build")); //Check if running in AndroidStudio + strRawFolder = "src" + File.separator + "main" + File.separator + "res" + File.separator + "raw"; + + File rawFolder = new File(strRawFolder); + if (!rawFolder.exists()) { + rawFolder.mkdirs(); + } + + System.out.println("Done building jar file. Creating dex file."); + if (onWindows) { + cmd = new String[]{ + "cmd", "/C", + "dx --dex --output=" + strRawFolder + File.separator + "anbuild.dex " + + builtPath + File.separator + "anbuild.jar" + }; + } else { + cmd = new String[]{ + getPathToDx(), + "--dex", + "--output=" + strRawFolder + File.separator + "anbuild.dex", + builtPath + File.separator + "anbuild.jar" + }; + } + ProcessBuilder dexBuilder = new ProcessBuilder(cmd); + try { + dexBuilder.start().waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + } + } + System.out.println("All done. ::: anbuild.dex should now be in your project's src" + File.separator + "main" + File.separator + "res" + File.separator + "raw" + File.separator + " folder :::"); + } + + protected void lookup(File path, List fileList) { + String desourcedPath = path.toString().replace("src" + File.separator, "").replace("main" + File.separator + "java" + File.separator, ""); + + File[] files = path.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + return true; + } + }); + for (File file : files) { + if (file.isDirectory()) { + if (-1 == file.getAbsolutePath().indexOf(AVOIDDIRPATH)) { + lookup(file, fileList); + } + } else { + if (file.getName().endsWith(".java")) { + if (hasClassAnnotation(file)) { + final String fileNamePrefix = file.getName().replace(".java", ""); + final File compiledPath = new File(getBuiltPath().toString() + File.separator + desourcedPath); + File[] classAndInnerClassFiles = compiledPath.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return filename.startsWith(fileNamePrefix); + } + }); + for (final File matchingFile : classAndInnerClassFiles) { + fileList.add(new File(desourcedPath + File.separator + matchingFile.getName())); + } + + } + } + } + } + } + + protected boolean hasClassAnnotation(File file) { + READ_STATE readState = READ_STATE.STARTING; + Pattern p = Pattern.compile(" class ([A-Za-z0-9_]+)"); + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + String line; + while (null != (line = reader.readLine())) { + switch (readState) { + case STARTING: + if (-1 < line.indexOf("@RootClass.Candidate")) { + readState = READ_STATE.FOUND_ANNOTATION; + } + break; + case FOUND_ANNOTATION: + Matcher m = p.matcher(line); + if (m.find()) { + System.out.println(" Found annotated class: " + m.group(0)); + return true; + } else { + System.err.println("Error: unmatched annotation in " + + file.getAbsolutePath()); + readState = READ_STATE.STARTING; + } + break; + } + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + protected String getPathToDx() throws IOException { + String androidHome = System.getenv("ANDROID_HOME"); + if (null == androidHome) { + throw new IOException("Error: you need to set $ANDROID_HOME globally"); + } + String dxPath = null; + File[] files = new File(androidHome + File.separator + "build-tools").listFiles(); + int recentSdkVersion = 0; + for (File file : files) { + + String fileName = null; + if (file.getName().contains("-")) { + String[] splitFileName = file.getName().split("-"); + if (splitFileName[1].contains("W")) { + char[] fileNameChars = splitFileName[1].toCharArray(); + fileName = String.valueOf(fileNameChars[0]); + } else if (splitFileName[1].contains("rc")) { + continue; //Do not use release candidates + } else { + fileName = splitFileName[1]; + } + } else { + fileName = file.getName(); + } + + int sdkVersion; + + String[] sdkVersionBits = fileName.split("[.]"); + sdkVersion = Integer.parseInt(sdkVersionBits[0]) * 10000; + if (sdkVersionBits.length > 1) { + sdkVersion += Integer.parseInt(sdkVersionBits[1]) * 100; + if (sdkVersionBits.length > 2) { + sdkVersion += Integer.parseInt(sdkVersionBits[2]); + } + } + if (sdkVersion > recentSdkVersion) { + String tentativePath = file.getAbsolutePath() + File.separator + "dx"; + if (new File(tentativePath).exists()) { + recentSdkVersion = sdkVersion; + dxPath = tentativePath; + } + } + } + if (dxPath == null) { + throw new IOException("Error: unable to find dx binary in $ANDROID_HOME"); + } + return dxPath; + } + + protected File getBuiltPath() { + File foundPath = null; + + File ideaPath = new File("out" + File.separator + "production"); // IntelliJ + if (ideaPath.isDirectory()) { + File[] children = ideaPath.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + }); + if (children.length > 0) { + foundPath = new File(ideaPath.getAbsolutePath() + File.separator + children[0].getName()); + } + } + if (null == foundPath) { + File eclipsePath = new File("bin" + File.separator + "classes"); // Eclipse IDE + if (eclipsePath.isDirectory()) { + foundPath = eclipsePath; + } + } + if (null == foundPath) { + File androidStudioPath = new File("build" + File.separator + "intermediates" + File.separator + "classes" + File.separator + "debug"); // Android Studio + if (androidStudioPath.isDirectory()) { + foundPath = androidStudioPath; + } + } + + return foundPath; + } + + + } + + public static void main(String[] args) { + try { + if (args.length == 0) { + new AnnotationsFinder(); + } else { + new RootClass(args); + } + } catch (Exception e) { + displayError(e); + } + } +} \ No newline at end of file diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/exceptions/RootDeniedException.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/exceptions/RootDeniedException.java new file mode 100644 index 0000000..758ba8c --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/exceptions/RootDeniedException.java @@ -0,0 +1,32 @@ +/* + * This file is part of the RootShell Project: https://github.com/Stericson/RootShell + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.RootShell.exceptions; + +public class RootDeniedException extends Exception { + + private static final long serialVersionUID = -8713947214162841310L; + + public RootDeniedException(String error) { + super(error); + } +} diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Command.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Command.java new file mode 100644 index 0000000..edb7bad --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Command.java @@ -0,0 +1,350 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.RootShell.execution; + +import com.stericson.RootShell.RootShell; + +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; + +public class Command { + + //directly modified by JavaCommand + protected boolean javaCommand = false; + protected Context context = null; + + public int totalOutput = 0; + + public int totalOutputProcessed = 0; + + ExecutionMonitor executionMonitor = null; + + Handler mHandler = null; + + //Has this command already been used? + protected boolean used = false; + + boolean executing = false; + + String[] command = {}; + + boolean finished = false; + + boolean terminated = false; + + boolean handlerEnabled = true; + + int exitCode = -1; + + int id = 0; + + int timeout = RootShell.defaultCommandTimeout; + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param command the command, or commands, to be executed. + */ + public Command(int id, String... command) { + this.command = command; + this.id = id; + + createHandler(RootShell.handlerEnabled); + } + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param handlerEnabled when true the handler will be used to call the + * callback methods if possible. + * @param command the command, or commands, to be executed. + */ + public Command(int id, boolean handlerEnabled, String... command) { + this.command = command; + this.id = id; + + createHandler(handlerEnabled); + } + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param timeout the time allowed before the shell will give up executing the command + * and throw a TimeoutException. + * @param command the command, or commands, to be executed. + */ + public Command(int id, int timeout, String... command) { + this.command = command; + this.id = id; + this.timeout = timeout; + + createHandler(RootShell.handlerEnabled); + } + + //If you override this you MUST make a final call + //to the super method. The super call should be the last line of this method. + public void commandOutput(int id, String line) { + RootShell.log("Command", "ID: " + id + ", " + line); + totalOutputProcessed++; + } + + public void commandTerminated(int id, String reason) { + //pass + } + + public void commandCompleted(int id, int exitcode) { + //pass + } + + protected final void commandFinished() { + if (!terminated) { + synchronized (this) { + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_COMPLETED); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandCompleted(id, exitCode); + } + + RootShell.log("Command " + id + " finished."); + finishCommand(); + } + } + } + + private void createHandler(boolean handlerEnabled) { + + this.handlerEnabled = handlerEnabled; + + if (Looper.myLooper() != null && handlerEnabled) { + RootShell.log("CommandHandler created"); + mHandler = new CommandHandler(); + } else { + RootShell.log("CommandHandler not created"); + } + } + + public final void finish() + { + RootShell.log("Command finished at users request!"); + commandFinished(); + } + + protected final void finishCommand() { + this.executing = false; + this.finished = true; + this.notifyAll(); + } + + + public final String getCommand() { + StringBuilder sb = new StringBuilder(); + + if(javaCommand) { + String filePath = context.getFilesDir().getPath(); + + for (int i = 0; i < command.length; i++) { + /* + * TODO Make withFramework optional for applications + * that do not require access to the fw. -CFR + */ + //export CLASSPATH=/data/user/0/ch.masshardt.emailnotification/files/anbuild.dex ; app_process /system/bin + if (Build.VERSION.SDK_INT > 22) { + //dalvikvm command is not working in Android Marshmallow + sb.append( + "export CLASSPATH=" + filePath + "/anbuild.dex;" + + " app_process /system/bin " + + command[i]); + } else { + sb.append( + "dalvikvm -cp " + filePath + "/anbuild.dex" + + " com.android.internal.util.WithFramework" + + " com.stericson.RootTools.containers.RootClass " + + command[i]); + } + + sb.append('\n'); + } + } + else { + for (int i = 0; i < command.length; i++) { + sb.append(command[i]); + sb.append('\n'); + } + } + + return sb.toString(); + } + + public final boolean isExecuting() { + return executing; + } + + public final boolean isHandlerEnabled() { + return handlerEnabled; + } + + public final boolean isFinished() { + return finished; + } + + public final int getExitCode() { + return this.exitCode; + } + + protected final void setExitCode(int code) { + synchronized (this) { + exitCode = code; + } + } + + protected final void startExecution() { + this.used = true; + executionMonitor = new ExecutionMonitor(this); + executionMonitor.setPriority(Thread.MIN_PRIORITY); + executionMonitor.start(); + executing = true; + } + + public final void terminate() + { + RootShell.log("Terminating command at users request!"); + terminated("Terminated at users request!"); + } + + protected final void terminate(String reason) { + try { + Shell.closeAll(); + RootShell.log("Terminating all shells."); + terminated(reason); + } catch (IOException e) { + } + } + + protected final void terminated(String reason) { + synchronized (Command.this) { + + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_TERMINATED); + bundle.putString(CommandHandler.TEXT, reason); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandTerminated(id, reason); + } + + RootShell.log("Command " + id + " did not finish because it was terminated. Termination reason: " + reason); + setExitCode(-1); + terminated = true; + finishCommand(); + } + } + + protected final void output(int id, String line) { + totalOutput++; + + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_OUTPUT); + bundle.putString(CommandHandler.TEXT, line); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandOutput(id, line); + } + } + + private class ExecutionMonitor extends Thread { + + private final Command command; + + public ExecutionMonitor(Command command) { + this.command = command; + } + + public void run() { + + if(command.timeout > 0) + { + synchronized (command) { + try { + RootShell.log("Command " + command.id + " is waiting for: " + command.timeout); + command.wait(command.timeout); + } catch (InterruptedException e) { + RootShell.log("Exception: " + e); + } + + if (!command.isFinished()) { + RootShell.log("Timeout Exception has occurred for command: " + command.id + "."); + terminate("Timeout Exception"); + } + } + } + } + } + + private class CommandHandler extends Handler { + + static final public String ACTION = "action"; + + static final public String TEXT = "text"; + + static final public int COMMAND_OUTPUT = 0x01; + + static final public int COMMAND_COMPLETED = 0x02; + + static final public int COMMAND_TERMINATED = 0x03; + + public final void handleMessage(Message msg) { + int action = msg.getData().getInt(ACTION); + String text = msg.getData().getString(TEXT); + + switch (action) { + case COMMAND_OUTPUT: + commandOutput(id, text); + break; + case COMMAND_COMPLETED: + commandCompleted(id, exitCode); + break; + case COMMAND_TERMINATED: + commandTerminated(id, text); + break; + } + } + } +} diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/JavaCommand.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/JavaCommand.java new file mode 100644 index 0000000..032569e --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/JavaCommand.java @@ -0,0 +1,58 @@ +package com.stericson.RootShell.execution; + +import android.content.Context; + +public class JavaCommand extends Command +{ + /** + * Constructor for executing Java commands rather than binaries + * + * @param context needed to execute java command. + */ + public JavaCommand(int id, Context context, String... command) { + super(id, command); + this.context = context; + this.javaCommand = true; + } + + /** + * Constructor for executing Java commands rather than binaries + * + * @param context needed to execute java command. + */ + public JavaCommand(int id, boolean handlerEnabled, Context context, String... command) { + super(id, handlerEnabled, command); + this.context = context; + this.javaCommand = true; + } + + /** + * Constructor for executing Java commands rather than binaries + * + * @param context needed to execute java command. + */ + public JavaCommand(int id, int timeout, Context context, String... command) { + super(id, timeout, command); + this.context = context; + this.javaCommand = true; + } + + + @Override + public void commandOutput(int id, String line) + { + super.commandOutput(id, line); + } + + @Override + public void commandTerminated(int id, String reason) + { + // pass + } + + @Override + public void commandCompleted(int id, int exitCode) + { + // pass + } +} diff --git a/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Shell.java b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Shell.java new file mode 100644 index 0000000..b64923e --- /dev/null +++ b/wrappers/android/mynteye/libshell/src/main/java/com/stericson/RootShell/execution/Shell.java @@ -0,0 +1,1038 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ +package com.stericson.RootShell.execution; + +import com.stericson.RootShell.RootShell; +import com.stericson.RootShell.exceptions.RootDeniedException; + +import android.content.Context; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; + +public class Shell { + + public static enum ShellType { + NORMAL, + ROOT, + CUSTOM + } + + //this is only used with root shells + public static enum ShellContext { + NORMAL("normal"), //The normal context... + SHELL("u:r:shell:s0"), //unprivileged shell (such as an adb shell) + SYSTEM_SERVER("u:r:system_server:s0"), // system_server, u:r:system:s0 on some firmwares + SYSTEM_APP("u:r:system_app:s0"), // System apps + PLATFORM_APP("u:r:platform_app:s0"), // System apps + UNTRUSTED_APP("u:r:untrusted_app:s0"), // Third-party apps + RECOVERY("u:r:recovery:s0"), //Recovery + SUPERSU("u:r:supersu:s0"); //SUPER SU default + + private String value; + + private ShellContext(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + } + + //Statics -- visible to all + private static final String token = "F*D^W@#FGF"; + + private static Shell rootShell = null; + + private static Shell shell = null; + + private static Shell customShell = null; + + private static String[] suVersion = new String[]{ + null, null + }; + + //the default context for root shells... + public static ShellContext defaultContext = ShellContext.NORMAL; + + //per shell + private int shellTimeout = 25000; + + private ShellType shellType = null; + + private ShellContext shellContext = Shell.ShellContext.NORMAL; + + private String error = ""; + + private final Process proc; + + private final BufferedReader inputStream; + + private final BufferedReader errorStream; + + private final OutputStreamWriter outputStream; + + private final List commands = new ArrayList(); + + //indicates whether or not to close the shell + private boolean close = false; + + private Boolean isSELinuxEnforcing = null; + + public boolean isExecuting = false; + + public boolean isReading = false; + + public boolean isClosed = false; + + private int maxCommands = 5000; + + private int read = 0; + + private int write = 0; + + private int totalExecuted = 0; + + private int totalRead = 0; + + private boolean isCleaning = false; + + private Shell(String cmd, ShellType shellType, ShellContext shellContext, int shellTimeout) throws IOException, TimeoutException, RootDeniedException { + + RootShell.log("Starting shell: " + cmd); + RootShell.log("Context: " + shellContext.getValue()); + RootShell.log("Timeout: " + shellTimeout); + + this.shellType = shellType; + this.shellTimeout = shellTimeout > 0 ? shellTimeout : this.shellTimeout; + this.shellContext = shellContext; + + if (this.shellContext == ShellContext.NORMAL) { + this.proc = Runtime.getRuntime().exec(cmd); + } else { + String display = getSuVersion(false); + String internal = getSuVersion(true); + + //only done for root shell... + //Right now only SUPERSU supports the --context switch + if (isSELinuxEnforcing() && + (display != null) && + (internal != null) && + (display.endsWith("SUPERSU")) && + (Integer.valueOf(internal) >= 190)) { + cmd += " --context " + this.shellContext.getValue(); + } else { + RootShell.log("Su binary --context switch not supported!"); + RootShell.log("Su binary display version: " + display); + RootShell.log("Su binary internal version: " + internal); + RootShell.log("SELinuxEnforcing: " + isSELinuxEnforcing()); + } + + this.proc = Runtime.getRuntime().exec(cmd); + + } + + this.inputStream = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), "UTF-8")); + this.errorStream = new BufferedReader(new InputStreamReader(this.proc.getErrorStream(), "UTF-8")); + this.outputStream = new OutputStreamWriter(this.proc.getOutputStream(), "UTF-8"); + + /** + * Thread responsible for carrying out the requested operations + */ + Worker worker = new Worker(this); + worker.start(); + + try { + /** + * The flow of execution will wait for the thread to die or wait until the + * given timeout has expired. + * + * The result of the worker, which is determined by the exit code of the worker, + * will tell us if the operation was completed successfully or it the operation + * failed. + */ + worker.join(this.shellTimeout); + + /** + * The operation could not be completed before the timeout occurred. + */ + if (worker.exit == -911) { + + try { + this.proc.destroy(); + } catch (Exception e) { + } + + closeQuietly(this.inputStream); + closeQuietly(this.errorStream); + closeQuietly(this.outputStream); + + throw new TimeoutException(this.error); + } + /** + * Root access denied? + */ + else if (worker.exit == -42) { + + try { + this.proc.destroy(); + } catch (Exception e) { + } + + closeQuietly(this.inputStream); + closeQuietly(this.errorStream); + closeQuietly(this.outputStream); + + throw new RootDeniedException("Root Access Denied"); + } + /** + * Normal exit + */ + else { + /** + * The shell is open. + * + * Start two threads, one to handle the input and one to handle the output. + * + * input, and output are runnables that the threads execute. + */ + Thread si = new Thread(this.input, "Shell Input"); + si.setPriority(Thread.NORM_PRIORITY); + si.start(); + + Thread so = new Thread(this.output, "Shell Output"); + so.setPriority(Thread.NORM_PRIORITY); + so.start(); + } + } catch (InterruptedException ex) { + worker.interrupt(); + Thread.currentThread().interrupt(); + throw new TimeoutException(); + } + } + + + public Command add(Command command) throws IOException { + if (this.close) { + throw new IllegalStateException( + "Unable to add commands to a closed shell"); + } + + if(command.used) { + //The command has been used, don't re-use... + throw new IllegalStateException( + "This command has already been executed. (Don't re-use command instances.)"); + } + + while (this.isCleaning) { + //Don't add commands while cleaning + ; + } + + this.commands.add(command); + + this.notifyThreads(); + + return command; + } + + public final void useCWD(Context context) throws IOException, TimeoutException, RootDeniedException { + add( + new Command( + -1, + false, + "cd " + context.getApplicationInfo().dataDir) + ); + } + + private void cleanCommands() { + this.isCleaning = true; + int toClean = Math.abs(this.maxCommands - (this.maxCommands / 4)); + RootShell.log("Cleaning up: " + toClean); + + for (int i = 0; i < toClean; i++) { + this.commands.remove(0); + } + + this.read = this.commands.size() - 1; + this.write = this.commands.size() - 1; + this.isCleaning = false; + } + + private void closeQuietly(final Reader input) { + try { + if (input != null) { + input.close(); + } + } catch (Exception ignore) { + } + } + + private void closeQuietly(final Writer output) { + try { + if (output != null) { + output.close(); + } + } catch (Exception ignore) { + } + } + + public void close() throws IOException { + RootShell.log("Request to close shell!"); + + int count = 0; + while (isExecuting) { + RootShell.log("Waiting on shell to finish executing before closing..."); + count++; + + //fail safe + if (count > 10000) { + break; + } + + } + + synchronized (this.commands) { + /** + * instruct the two threads monitoring input and output + * of the shell to close. + */ + this.close = true; + this.notifyThreads(); + } + + RootShell.log("Shell Closed!"); + + if (this == Shell.rootShell) { + Shell.rootShell = null; + } else if (this == Shell.shell) { + Shell.shell = null; + } else if (this == Shell.customShell) { + Shell.customShell = null; + } + } + + public static void closeCustomShell() throws IOException { + RootShell.log("Request to close custom shell!"); + + if (Shell.customShell == null) { + return; + } + + Shell.customShell.close(); + } + + public static void closeRootShell() throws IOException { + RootShell.log("Request to close root shell!"); + + if (Shell.rootShell == null) { + return; + } + Shell.rootShell.close(); + } + + public static void closeShell() throws IOException { + RootShell.log("Request to close normal shell!"); + + if (Shell.shell == null) { + return; + } + Shell.shell.close(); + } + + public static void closeAll() throws IOException { + RootShell.log("Request to close all shells!"); + + Shell.closeShell(); + Shell.closeRootShell(); + Shell.closeCustomShell(); + } + + public int getCommandQueuePosition(Command cmd) { + return this.commands.indexOf(cmd); + } + + public String getCommandQueuePositionString(Command cmd) { + return "Command is in position " + getCommandQueuePosition(cmd) + " currently executing command at position " + this.write + " and the number of commands is " + commands.size(); + } + + public static Shell getOpenShell() { + if (Shell.customShell != null) { + return Shell.customShell; + } else if (Shell.rootShell != null) { + return Shell.rootShell; + } else { + return Shell.shell; + } + } + + /** + * From libsuperuser. + * + *

+ * Detects the version of the su binary installed (if any), if supported + * by the binary. Most binaries support two different version numbers, + * the public version that is displayed to users, and an internal + * version number that is used for version number comparisons. Returns + * null if su not available or retrieving the version isn't supported. + *

+ *

+ * Note that su binary version and GUI (APK) version can be completely + * different. + *

+ *

+ * This function caches its result to improve performance on multiple + * calls + *

+ * + * @param internal Request human-readable version or application + * internal version + * @return String containing the su version or null + */ + private synchronized String getSuVersion(boolean internal) { + int idx = internal ? 0 : 1; + if (suVersion[idx] == null) { + String version = null; + + // Replace libsuperuser:Shell.run with manual process execution + Process process; + try { + process = Runtime.getRuntime().exec(internal ? "su -V" : "su -v", null); + process.waitFor(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (InterruptedException e) { + e.printStackTrace(); + return null; + } + + // From libsuperuser:StreamGobbler + List stdout = new ArrayList(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + try { + String line = null; + while ((line = reader.readLine()) != null) { + stdout.add(line); + } + } catch (IOException e) { + } + // make sure our stream is closed and resources will be freed + try { + reader.close(); + } catch (IOException e) { + } + + process.destroy(); + + List ret = stdout; + + if (ret != null) { + for (String line : ret) { + if (!internal) { + if (line.contains(".")) { + version = line; + break; + } + } else { + try { + if (Integer.parseInt(line) > 0) { + version = line; + break; + } + } catch (NumberFormatException e) { + } + } + } + } + + suVersion[idx] = version; + } + return suVersion[idx]; + } + + public static boolean isShellOpen() { + return Shell.shell == null; + } + + public static boolean isCustomShellOpen() { + return Shell.customShell == null; + } + + public static boolean isRootShellOpen() { + return Shell.rootShell == null; + } + + public static boolean isAnyShellOpen() { + return Shell.shell != null || Shell.rootShell != null || Shell.customShell != null; + } + + /** + * From libsuperuser. + * + * Detect if SELinux is set to enforcing, caches result + * + * @return true if SELinux set to enforcing, or false in the case of + * permissive or not present + */ + public synchronized boolean isSELinuxEnforcing() { + if (isSELinuxEnforcing == null) { + Boolean enforcing = null; + + // First known firmware with SELinux built-in was a 4.2 (17) + // leak + if (android.os.Build.VERSION.SDK_INT >= 17) { + + // Detect enforcing through sysfs, not always present + File f = new File("/sys/fs/selinux/enforce"); + if (f.exists()) { + try { + InputStream is = new FileInputStream("/sys/fs/selinux/enforce"); + try { + enforcing = (is.read() == '1'); + } finally { + is.close(); + } + } catch (Exception e) { + } + } + + // 4.4+ builds are enforcing by default, take the gamble + if (enforcing == null) { + enforcing = (android.os.Build.VERSION.SDK_INT >= 19); + } + } + + if (enforcing == null) { + enforcing = false; + } + + isSELinuxEnforcing = enforcing; + } + return isSELinuxEnforcing; + } + + /** + * Runnable to write commands to the open shell. + *

+ * When writing commands we stay in a loop and wait for new + * commands to added to "commands" + *

+ * The notification of a new command is handled by the method add in this class + */ + private Runnable input = new Runnable() { + public void run() { + + try { + while (true) { + + synchronized (commands) { + /** + * While loop is used in the case that notifyAll is called + * and there are still no commands to be written, a rare + * case but one that could happen. + */ + while (!close && write >= commands.size()) { + isExecuting = false; + commands.wait(); + } + } + + if (write >= maxCommands) { + + /** + * wait for the read to catch up. + */ + while (read != write) { + RootShell.log("Waiting for read and write to catch up before cleanup."); + } + /** + * Clean up the commands, stay neat. + */ + cleanCommands(); + } + + /** + * Write the new command + * + * We write the command followed by the token to indicate + * the end of the command execution + */ + if (write < commands.size()) { + isExecuting = true; + Command cmd = commands.get(write); + + if(null != cmd) + { + cmd.startExecution(); + RootShell.log("Executing: " + cmd.getCommand() + " with context: " + shellContext); + + //write the command + outputStream.write(cmd.getCommand()); + outputStream.flush(); + + //write the token... + String line = "\necho " + token + " " + totalExecuted + " $?\n"; + outputStream.write(line); + outputStream.flush(); + + write++; + totalExecuted++; + } + } else if (close) { + /** + * close the thread, the shell is closing. + */ + isExecuting = false; + outputStream.write("\nexit 0\n"); + outputStream.flush(); + RootShell.log("Closing shell"); + return; + } + } + } catch (IOException | InterruptedException e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } + finally { + write = 0; + closeQuietly(outputStream); + } + } + }; + + protected void notifyThreads() { + Thread t = new Thread() { + public void run() { + synchronized (commands) { + commands.notifyAll(); + } + } + }; + + t.start(); + } + + /** + * Runnable to monitor the responses from the open shell. + * + * This include the output and error stream + */ + private Runnable output = new Runnable() { + public void run() { + try { + Command command = null; + + //as long as there is something to read, we will keep reading. + while (!close || inputStream.ready() || read < commands.size()) { + isReading = false; + String outputLine = inputStream.readLine(); + isReading = true; + + /** + * If we receive EOF then the shell closed? + */ + if (outputLine == null) { + break; + } + + if (command == null) { + if (read >= commands.size()) { + if (close) { + break; + } + + continue; + } + + command = commands.get(read); + } + + /** + * trying to determine if all commands have been completed. + * + * if the token is present then the command has finished execution. + */ + int pos = -1; + + pos = outputLine.indexOf(token); + + if (pos == -1) { + /** + * send the output for the implementer to process + */ + command.output(command.id, outputLine); + } else if (pos > 0) { + /** + * token is suffix of output, send output part to implementer + */ + RootShell.log("Found token, line: " + outputLine); + command.output(command.id, outputLine.substring(0, pos)); + } + + if (pos >= 0) { + outputLine = outputLine.substring(pos); + String fields[] = outputLine.split(" "); + + if (fields.length >= 2 && fields[1] != null) { + int id = 0; + + try { + id = Integer.parseInt(fields[1]); + } catch (NumberFormatException e) { + } + + int exitCode = -1; + + try { + exitCode = Integer.parseInt(fields[2]); + } catch (NumberFormatException e) { + } + + if (id == totalRead) { + processErrors(command); + + + /** + * wait for output to be processed... + * + */ + int iterations = 0; + while (command.totalOutput > command.totalOutputProcessed) { + + if(iterations == 0) + { + iterations++; + RootShell.log("Waiting for output to be processed. " + command.totalOutputProcessed + " Of " + command.totalOutput); + } + + try { + + synchronized (this) + { + this.wait(2000); + } + } catch (Exception e) { + RootShell.log(e.getMessage()); + } + } + + RootShell.log("Read all output"); + + command.setExitCode(exitCode); + command.commandFinished(); + + command = null; + + read++; + totalRead++; + continue; + } + } + } + } + + try { + proc.waitFor(); + proc.destroy(); + } catch (Exception e) { + } + + while (read < commands.size()) { + if (command == null) { + command = commands.get(read); + } + + if(command.totalOutput < command.totalOutputProcessed) + { + command.terminated("All output not processed!"); + command.terminated("Did you forget the super.commandOutput call or are you waiting on the command object?"); + } + else + { + command.terminated("Unexpected Termination."); + } + + command = null; + read++; + } + + read = 0; + + } catch (IOException e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } finally { + closeQuietly(outputStream); + closeQuietly(errorStream); + closeQuietly(inputStream); + + RootShell.log("Shell destroyed"); + isClosed = true; + isReading = false; + } + } + }; + + public void processErrors(Command command) { + try { + while (errorStream.ready() && command != null) { + String line = errorStream.readLine(); + + /** + * If we recieve EOF then the shell closed? + */ + if (line == null) { + break; + } + + /** + * send the output for the implementer to process + */ + command.output(command.id, line); + } + } catch (Exception e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } + } + + public static Command runRootCommand(Command command) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell().add(command); + } + + public static Command runCommand(Command command) throws IOException, TimeoutException { + return Shell.startShell().add(command); + } + + public static Shell startRootShell() throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(0, 3); + } + + public static Shell startRootShell(int timeout) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(timeout, 3); + } + + public static Shell startRootShell(int timeout, int retry) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(timeout, Shell.defaultContext, retry); + } + + public static Shell startRootShell(int timeout, ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { + // keep prompting the user until they accept for x amount of times... + int retries = 0; + + if (Shell.rootShell == null) { + + RootShell.log("Starting Root Shell!"); + String cmd = "su"; + while (Shell.rootShell == null) { + try { + RootShell.log("Trying to open Root Shell, attempt #" + retries); + Shell.rootShell = new Shell(cmd, ShellType.ROOT, shellContext, timeout); + } catch (IOException e) { + if (retries++ >= retry) { + RootShell.log("IOException, could not start shell"); + throw e; + } + } catch (RootDeniedException e) { + if (retries++ >= retry) { + RootShell.log("RootDeniedException, could not start shell"); + throw e; + } + } catch (TimeoutException e) { + if (retries++ >= retry) { + RootShell.log("TimeoutException, could not start shell"); + throw e; + } + } + } + } else if (Shell.rootShell.shellContext != shellContext) { + try { + RootShell.log("Context is different than open shell, switching context... " + Shell.rootShell.shellContext + " VS " + shellContext); + Shell.rootShell.switchRootShellContext(shellContext); + } catch (IOException e) { + if (retries++ >= retry) { + RootShell.log("IOException, could not switch context!"); + throw e; + } + } catch (RootDeniedException e) { + if (retries++ >= retry) { + RootShell.log("RootDeniedException, could not switch context!"); + throw e; + } + } catch (TimeoutException e) { + if (retries++ >= retry) { + RootShell.log("TimeoutException, could not switch context!"); + throw e; + } + } + } else { + RootShell.log("Using Existing Root Shell!"); + } + + return Shell.rootShell; + } + + public static Shell startCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException { + return Shell.startCustomShell(shellPath, 0); + } + + public static Shell startCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException { + + if (Shell.customShell == null) { + RootShell.log("Starting Custom Shell!"); + Shell.customShell = new Shell(shellPath, ShellType.CUSTOM, ShellContext.NORMAL, timeout); + } else { + RootShell.log("Using Existing Custom Shell!"); + } + + return Shell.customShell; + } + + public static Shell startShell() throws IOException, TimeoutException { + return Shell.startShell(0); + } + + public static Shell startShell(int timeout) throws IOException, TimeoutException { + + try { + if (Shell.shell == null) { + RootShell.log("Starting Shell!"); + Shell.shell = new Shell("/system/bin/sh", ShellType.NORMAL, ShellContext.NORMAL, timeout); + } else { + RootShell.log("Using Existing Shell!"); + } + return Shell.shell; + } catch (RootDeniedException e) { + //Root Denied should never be thrown. + throw new IOException(); + } + } + + public Shell switchRootShellContext(ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + if (this.shellType == ShellType.ROOT) { + try { + Shell.closeRootShell(); + } catch (Exception e) { + RootShell.log("Problem closing shell while trying to switch context..."); + } + + //create new root shell with new context... + + return Shell.startRootShell(this.shellTimeout, shellContext, 3); + } else { + //can only switch context on a root shell... + RootShell.log("Can only switch context on a root shell!"); + return this; + } + } + + protected static class Worker extends Thread { + + public int exit = -911; + + public Shell shell; + + private Worker(Shell shell) { + this.shell = shell; + } + + public void run() { + + /** + * Trying to open the shell. + * + * We echo "Started" and we look for it in the output. + * + * If we find the output then the shell is open and we return. + * + * If we do not find it then we determine the error and report + * it by setting the value of the variable exit + */ + try { + shell.outputStream.write("echo Started\n"); + shell.outputStream.flush(); + + while (true) { + String line = shell.inputStream.readLine(); + + if (line == null) { + throw new EOFException(); + } else if ("".equals(line)) { + continue; + } else if ("Started".equals(line)) { + this.exit = 1; + setShellOom(); + break; + } + + shell.error = "unknown error occurred."; + } + } catch (IOException e) { + exit = -42; + if (e.getMessage() != null) { + shell.error = e.getMessage(); + } else { + shell.error = "RootAccess denied?."; + } + } + + } + + /* + * setOom for shell processes (sh and su if root shell) and discard outputs + * Negative values make the process LESS likely to be killed in an OOM situation + * Positive values make the process MORE likely to be killed in an OOM situation + */ + private void setShellOom() { + try { + Class processClass = shell.proc.getClass(); + Field field; + try { + field = processClass.getDeclaredField("pid"); + } catch (NoSuchFieldException e) { + field = processClass.getDeclaredField("id"); + } + field.setAccessible(true); + int pid = (Integer) field.get(shell.proc); + shell.outputStream.write("(echo -17 > /proc/" + pid + "/oom_adj) &> /dev/null\n"); + shell.outputStream.write("(echo -17 > /proc/$$/oom_adj) &> /dev/null\n"); + shell.outputStream.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/wrappers/android/mynteye/settings.gradle b/wrappers/android/mynteye/settings.gradle index f04decd..7804b01 100644 --- a/wrappers/android/mynteye/settings.gradle +++ b/wrappers/android/mynteye/settings.gradle @@ -1 +1 @@ -include ':app', ':libmynteye' +include ':app', ':libmynteye', ':libshell'