├── .github ├── FUNDING.yml └── workflows │ ├── android.yml │ └── gradle-wrapper-validation.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── core ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── eu │ │ │ └── darken │ │ │ └── rxshell │ │ │ ├── cmd │ │ │ ├── Cmd.java │ │ │ ├── CmdProcessor.java │ │ │ ├── ErrorHarvester.java │ │ │ ├── Harvester.java │ │ │ ├── OutputHarvester.java │ │ │ └── RxCmdShell.java │ │ │ ├── extra │ │ │ ├── ApiWrap.java │ │ │ ├── CmdHelper.java │ │ │ ├── EnvVar.java │ │ │ ├── HasEnvironmentVariables.java │ │ │ ├── RXSDebug.java │ │ │ └── RxCmdShellHelper.java │ │ │ ├── process │ │ │ ├── DefaultProcessFactory.java │ │ │ ├── ProcessFactory.java │ │ │ ├── ProcessHelper.java │ │ │ ├── ProcessKiller.java │ │ │ ├── RootKiller.java │ │ │ ├── RxProcess.java │ │ │ └── UserKiller.java │ │ │ └── shell │ │ │ ├── LineReader.java │ │ │ └── RxShell.java │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ ├── eu │ └── darken │ │ └── rxshell │ │ ├── cmd │ │ ├── CmdBuilderTest.java │ │ ├── CmdProcessorTest.java │ │ ├── CmdResultTest.java │ │ ├── HarvesterTest.java │ │ ├── RxCmdShellBuilderTest.java │ │ ├── RxCmdShellHelperTest.java │ │ └── RxCmdShellTest.java │ │ ├── extra │ │ ├── ApiWrapTest.java │ │ └── RXSDebugTest.java │ │ ├── process │ │ ├── DefaultProcessFactoryTest.java │ │ ├── ProcessHelperTest.java │ │ ├── RootKillerTest.java │ │ ├── RxProcessTest.java │ │ └── UserKillerTest.java │ │ └── shell │ │ ├── LineReaderTest.java │ │ └── RxShellTest.java │ └── testtools │ ├── BaseTest.java │ ├── JUnitTree.java │ ├── MockInputStream.java │ ├── MockOutputStream.java │ ├── MockProcess.java │ ├── MockRxShellSession.java │ ├── StreamHelper.java │ └── TestHelper.java ├── example ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── eu │ │ │ └── darken │ │ │ └── rxshellexample │ │ │ ├── App.java │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── eu │ └── darken │ └── rxshellexample │ └── DummyTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lint.xml ├── publish-to-bintray.gradle ├── root ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── eu │ │ │ └── darken │ │ │ └── rxshell │ │ │ └── root │ │ │ ├── Root.java │ │ │ ├── RootContext.java │ │ │ ├── SELinux.java │ │ │ ├── SuApp.java │ │ │ └── SuBinary.java │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ ├── eu │ └── darken │ │ └── rxshell │ │ └── root │ │ ├── RootContextTest.java │ │ ├── RootTest.java │ │ ├── SELinuxText.java │ │ ├── SuAppTest.java │ │ └── SuBinaryTest.java │ └── testhelper │ ├── BaseTest.java │ ├── JUnitTree.java │ └── TestHelper.java └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: 4 | - "https://www.buymeacoffee.com/tydarken" 5 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'adopt' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Run tests 26 | run: ./gradlew testRelease 27 | - name: Build with Gradle 28 | run: ./gradlew assembleDebug 29 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | validation: 13 | name: "Validation" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: gradle/wrapper-validation-action@v1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/ 38 | 39 | # Keystore files 40 | *.jks 41 | 42 | # External native build folder generated in Android Studio 2.2 and later 43 | .externalNativeBuild 44 | 45 | # Google Services (e.g. APIs or Firebase) 46 | google-services.json 47 | 48 | # Freeline 49 | freeline.py 50 | freeline/ 51 | freeline_project_description.json 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributions are very welcome, but please take note of the following points: 2 | 3 | * Send your pull-request against the dev branch 4 | * Continious integration will test your code, make sure all tests pass 5 | * Adhere to the projects current code style 6 | * Try to add unit-tests for your new code 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RxShell 2 | 3 | ![Build](https://github.com/d4rken/RxShell/actions/workflows/android.yml/badge.svg) 4 | [ ![Download](https://jitpack.io/v/d4rken/rxshell.svg)](https://jitpack.io/#d4rken/rxshell) 5 | [ ![Coverage Status](https://coveralls.io/repos/github/d4rken/RxShell/badge.svg?branch=master)](https://coveralls.io/github/d4rken/RxShell?branch=master) 6 | 7 | A library that helps your app interact with shells on Android. 8 | 9 | ## Quickstart 10 | Include the library in your modules `build.gradle` file: 11 | ```groovy 12 | implementation 'eu.darken.rxshell:' 13 | ``` 14 | 15 | Now your project is ready to use the library, let's quickly talk about a few core concepts: 16 | 17 | 1. You construct a shell using `RxCmdShell.builder()`. 18 | 2. The shell has to be opened before use (`shell.open()`), which will launch the process and give you a `RxCmdShell.Session` to work with. 19 | 3. Build your commands with `Cmd.builder("your command")`. 20 | 4. Commands are run with `session.submit(command)`, `cmd.submit(session)` or `cmd.execute(session)`. 21 | 5. Remember to `close()` the session to release resources. 22 | 23 | ### Examples 24 | #### Single-Shot execution 25 | If you pass a shell builder to the command it will be used for a single shot execution. 26 | 27 | A shell is created and opened, used and closed. 28 | 29 | `Cmd.execute(...)` is shorthand for `Cmd.submit(...).blockingGet()`, so don't run it from a UI thread. 30 | 31 | ```java 32 | Cmd.Result result = Cmd.builder("echo hello").execute(RxCmdShell.builder()); 33 | ``` 34 | 35 | #### Reusing a shell 36 | If you want to issue multiple commands, you can reuse the shell which is faster and uses less resources. 37 | 38 | ```java 39 | RxCmdShell.Session session = RxCmdShell.builder().build().open().blockingGet(); 40 | // Blocking 41 | Cmd.Result result1 = Cmd.builder("echo straw").execute(session); 42 | // Async 43 | Cmd.builder("echo berry").submit(session).subscribe(result -> Log.i("ExitCode: " + result.getExitCode())); 44 | shell.close().blockingGet(); 45 | ``` 46 | 47 | The default shell process is launched using `sh`, if you want to open a root shell (using `su`) tell the ShellBuilder! 48 | ```java 49 | Cmd.Result result = Cmd.builder("echo hello").execute(RxCmdShell.builder().root(true)); 50 | ``` 51 | 52 | #### Checking root access 53 | ```java 54 | // General info 55 | new RootContext.Builder(getContext()).build().subscribe(c -> {/* c.getRoot().getState() */}); 56 | // Just root state 57 | Root root = new Root.Builder().build().blockingGet(); 58 | if(root.getState() == Root.State.ROOTED) /* yay */ 59 | 60 | ``` 61 | 62 | ## Used by 63 | * [SD Maid](https://github.com/d4rken/sdmaid-public), which was also the motivation for this library. 64 | 65 | ## Alternatives 66 | While this is obviously :^) the best library, there are alternatives you could be interested in: 67 | 68 | * [@Chainfire's](https://twitter.com/ChainfireXDA) [libsuperuser](https://github.com/Chainfire/libsuperuser) 69 | * [@SpazeDog's](https://github.com/SpazeDog) [rootfw](https://github.com/SpazeDog/rootfw) 70 | * [@topjohnwu's](https://twitter.com/topjohnwu) [libsu](https://github.com/topjohnwu/libsu) 71 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | def versionMajor = 3 3 | def versionMinor = 1 4 | def versionPatch = 0 5 | 6 | ext.versions = [ 7 | 'versionCode': versionMajor * 10000 + versionMinor * 100 + versionPatch, 8 | 'versionName': "${versionMajor}.${versionMinor}.${versionPatch}", 9 | 'minSdk' : 21, 10 | 'targetSdk' : 31, 11 | 'compileSdk' : 31, 12 | 'sourceCompatibility': JavaVersion.VERSION_1_8, 13 | 'targetCompatibility': JavaVersion.VERSION_1_8, 14 | 'supportLibrary' : '28.0.0', 15 | ] 16 | 17 | ext.bintrayConfig = [ 18 | bintrayRepo : 'maven', 19 | bintrayName : 'rxshell', 20 | publishedGroupId: 'eu.darken.rxshell', 21 | 22 | siteUrl : 'https://github.com/d4rken/RxShell', 23 | issuesUrl : 'https://github.com/d4rken/RxShell/issues', 24 | gitUrl : 'https://github.com/d4rken/RxShell.git', 25 | 26 | libraryVersion : versions.versionName, 27 | 28 | developerId : 'darken', 29 | developerName : 'Matthias Urhahn', 30 | developerEmail : 'darken@darken.eu', 31 | 32 | licenseName : 'Apache-2.0', 33 | licenseUrl : 'https://github.com/d4rken/RxShell/blob/master/LICENSE', 34 | allLicenses : ["Apache-2.0"] 35 | ] 36 | 37 | ext.deps = [ 38 | 'support' : [ 39 | 'annotations': "com.android.support:support-annotations:${versions.supportLibrary}", 40 | 'appcompat' : "com.android.support:appcompat-v7:${versions.supportLibrary}" 41 | ], 42 | androidPlugin: 'com.android.tools.build:gradle:7.0.4', 43 | timber : "com.jakewharton.timber:timber:4.7.1", 44 | rxJava : "io.reactivex.rxjava3:rxjava:3.1.3", 45 | rxJavaReplay : "com.jakewharton.rx3:replaying-share:3.0.0", 46 | jUnit : "junit:junit:4.12", 47 | mockito : "org.mockito:mockito-core:3.9.0", 48 | awaitility : "org.awaitility:awaitility:3.0.0", 49 | ] 50 | 51 | repositories { 52 | google() 53 | mavenCentral() 54 | jcenter() 55 | } 56 | dependencies { 57 | classpath 'com.android.tools.build:gradle:7.1.1' 58 | } 59 | } 60 | 61 | allprojects { 62 | repositories { 63 | google() 64 | jcenter() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileOptions { 5 | sourceCompatibility versions.sourceCompatibility 6 | targetCompatibility versions.targetCompatibility 7 | } 8 | 9 | compileSdkVersion versions.compileSdk 10 | 11 | defaultConfig { 12 | minSdkVersion versions.minSdk 13 | targetSdkVersion versions.targetSdk 14 | 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | debug { 20 | minifyEnabled true 21 | shrinkResources false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') 23 | } 24 | } 25 | lint { 26 | disable 'TimberTagLength' 27 | textOutput file('stdout') 28 | textReport true 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation deps.support.annotations 34 | 35 | implementation deps.timber 36 | 37 | api deps.rxJava 38 | api deps.rxJavaReplay 39 | 40 | testImplementation deps.jUnit 41 | testImplementation deps.mockito 42 | testImplementation deps.awaitility 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/cmd/CmdProcessor.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.cmd; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.LinkedBlockingDeque; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.TimeoutException; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | import eu.darken.rxshell.extra.RXSDebug; 14 | import eu.darken.rxshell.shell.RxShell; 15 | import io.reactivex.rxjava3.core.Observable; 16 | import io.reactivex.rxjava3.core.ObservableOnSubscribe; 17 | import io.reactivex.rxjava3.core.Observer; 18 | import io.reactivex.rxjava3.core.Single; 19 | import io.reactivex.rxjava3.core.SingleEmitter; 20 | import io.reactivex.rxjava3.core.SingleOnSubscribe; 21 | import io.reactivex.rxjava3.disposables.Disposable; 22 | import io.reactivex.rxjava3.schedulers.Schedulers; 23 | import io.reactivex.rxjava3.subjects.BehaviorSubject; 24 | import timber.log.Timber; 25 | 26 | public class CmdProcessor { 27 | static final String TAG = "RXS:CmdProcessor"; 28 | final Harvester.Factory factory; 29 | final BehaviorSubject idlePub = BehaviorSubject.createDefault(true); 30 | final LinkedBlockingDeque cmdQueue = new LinkedBlockingDeque<>(); 31 | final AtomicBoolean attached = new AtomicBoolean(false); 32 | volatile boolean dead = false; 33 | 34 | public CmdProcessor(Harvester.Factory factory) { 35 | this.factory = factory; 36 | } 37 | 38 | public Single submit(Cmd cmd) { 39 | return Single.create((SingleOnSubscribe) emitter -> { 40 | QueueCmd item = new QueueCmd(cmd, emitter); 41 | synchronized (CmdProcessor.this) { 42 | if (dead) { 43 | if (RXSDebug.isDebug()) Timber.tag(TAG).w("Processor wasn't running: %s", cmd); 44 | item.exitCode(Cmd.ExitCode.SHELL_DIED); 45 | item.emit(); 46 | } else { 47 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("Submitted: %s", cmd); 48 | cmdQueue.add(item); 49 | } 50 | } 51 | }).doOnSuccess(item -> { 52 | if (RXSDebug.isDebug()) { 53 | Timber.tag(TAG).log(item.getErrors() != null && item.getErrors().size() > 0 ? Log.WARN : Log.INFO, "Processed: %s", item); 54 | } 55 | }); 56 | } 57 | 58 | public synchronized void attach(RxShell.Session session) { 59 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("attach(%s)", session); 60 | if (attached.getAndSet(true)) throw new IllegalStateException("Processor is already attached!"); 61 | 62 | Observable 63 | .create((ObservableOnSubscribe) emitter -> { 64 | while (true) { 65 | QueueCmd item = cmdQueue.take(); 66 | if (item.isPoisonPill()) { 67 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("Poison pill!"); 68 | break; 69 | } else { 70 | idlePub.onNext(false); 71 | emitter.onNext(item); 72 | } 73 | } 74 | synchronized (CmdProcessor.this) { 75 | dead = true; 76 | while (!cmdQueue.isEmpty()) { 77 | final QueueCmd item = cmdQueue.poll(); 78 | if (item.isPoisonPill()) continue; 79 | item.exitCode(Cmd.ExitCode.SHELL_DIED); 80 | item.emit(); 81 | } 82 | } 83 | emitter.onComplete(); 84 | idlePub.onNext(true); 85 | idlePub.onComplete(); 86 | }) 87 | .subscribeOn(Schedulers.io()) 88 | .concatMap(item -> { 89 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("Processing: %s", item.cmd); 90 | final Observable outputs = session.outputLines() 91 | .compose(upstream -> factory.forOutput(upstream, item.cmd)) 92 | .doOnEach(n -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v("outputLine():doOnEach: %s", n); }) 93 | .toObservable().cache(); 94 | outputs.subscribe(s -> {}, e -> {}); 95 | 96 | final Observable errors = session.errorLines() 97 | .compose(upstream -> factory.forError(upstream, item.cmd)) 98 | .doOnEach(n -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v("errorLines():doOnEach: %s", n); }) 99 | .toObservable().cache(); 100 | errors.subscribe(s -> {}, e -> {}); 101 | 102 | try { 103 | for (String write : item.cmd.getCommands()) session.writeLine(write, false); 104 | session.writeLine("echo " + item.cmd.getMarker() + " $?", false); 105 | session.writeLine("echo " + item.cmd.getMarker() + " >&2", true); 106 | } catch (IOException e) { 107 | return Observable.just(item.exitCode(Cmd.ExitCode.SHELL_DIED)); 108 | } 109 | 110 | Observable cropWait = Observable.merge(outputs, errors) 111 | .toList().toObservable() 112 | .map(crops -> { 113 | boolean isComplete = true; 114 | for (Harvester.Crop crop : crops) { 115 | if (crop instanceof OutputHarvester.Crop) { 116 | item.exitCode(((OutputHarvester.Crop) crop).exitCode); 117 | item.output(crop.buffer); 118 | } else { 119 | item.errors(crop.buffer); 120 | } 121 | if (!crop.isComplete) isComplete = false; 122 | } 123 | if (crops.size() != 2 || !isComplete) item.exitCode(Cmd.ExitCode.SHELL_DIED); 124 | return item; 125 | }); 126 | if (item.cmd.getTimeout() > 0) { 127 | cropWait = cropWait.timeout(item.cmd.getTimeout(), TimeUnit.MILLISECONDS).onErrorReturn(error -> { 128 | if (error instanceof TimeoutException) { 129 | if (RXSDebug.isDebug()) Timber.tag(TAG).w("Command timed out: %s", item); 130 | return item.exitCode(Cmd.ExitCode.TIMEOUT); 131 | } else throw new RuntimeException(error); 132 | }); 133 | } 134 | return cropWait; 135 | }) 136 | .doOnEach(n -> { if (RXSDebug.isDebug()) Timber.tag(TAG).d("Post zip: %s", n); }) 137 | .subscribe(new Observer() { 138 | @Override 139 | public void onSubscribe(Disposable d) { 140 | session.waitFor().subscribeOn(Schedulers.io()).subscribe(integer -> { 141 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("Attached session ended!"); 142 | cmdQueue.add(QueueCmd.poisonPill()); 143 | }); 144 | } 145 | 146 | @Override 147 | public void onNext(QueueCmd item) { 148 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("onNext(%s)", item); 149 | if (item.exitCode < 0) { 150 | cmdQueue.addFirst(QueueCmd.poisonPill()); 151 | session.cancel().subscribe(); 152 | } 153 | item.resultEmitter.onSuccess(item.buildResult()); 154 | idlePub.onNext(cmdQueue.isEmpty()); 155 | } 156 | 157 | @Override 158 | public void onError(Throwable error) { 159 | if (RXSDebug.isDebug()) Timber.tag(TAG).v(error, "onError()"); 160 | } 161 | 162 | @Override 163 | public void onComplete() { 164 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("onComplete()"); 165 | } 166 | }); 167 | } 168 | 169 | public Observable isIdle() { 170 | return idlePub.doOnEach(n -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v("isIdle: %s", n);}); 171 | } 172 | 173 | static class QueueCmd { 174 | final Cmd cmd; 175 | final SingleEmitter resultEmitter; 176 | int exitCode = Cmd.ExitCode.INITIAL; 177 | List output; 178 | List errors; 179 | 180 | QueueCmd(Cmd cmd, SingleEmitter resultEmitter) { 181 | this.cmd = cmd; 182 | this.resultEmitter = resultEmitter; 183 | } 184 | 185 | QueueCmd exitCode(int exitCode) { 186 | this.exitCode = exitCode; 187 | return this; 188 | } 189 | 190 | QueueCmd output(List output) { 191 | this.output = output; 192 | return this; 193 | } 194 | 195 | QueueCmd errors(List errors) { 196 | this.errors = errors; 197 | return this; 198 | } 199 | 200 | Cmd.Result buildResult() { 201 | return new Cmd.Result( 202 | cmd, exitCode, 203 | output == null && cmd.isOutputBufferEnabled() ? new ArrayList<>() : output, 204 | errors == null && cmd.isErrorBufferEnabled() ? new ArrayList<>() : errors 205 | ); 206 | } 207 | 208 | void emit() { 209 | resultEmitter.onSuccess(buildResult()); 210 | } 211 | 212 | boolean isPoisonPill() { 213 | return cmd == null && resultEmitter == null; 214 | } 215 | 216 | static QueueCmd poisonPill() { 217 | return new QueueCmd(null, null); 218 | } 219 | 220 | @Override 221 | public String toString() { 222 | return "QueueCmd(command=" + cmd + ", exitCode=" + exitCode + ", output.size()=" + (output != null ? output.size() : null) + ", errors.size()=" + (errors != null ? errors.size() : null) + ")"; 223 | } 224 | } 225 | 226 | public static class Factory { 227 | private final Harvester.Factory harvesterFactory; 228 | 229 | public Factory(Harvester.Factory harvesterFactory) {this.harvesterFactory = harvesterFactory;} 230 | 231 | public CmdProcessor create() { 232 | return new CmdProcessor(harvesterFactory); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/cmd/ErrorHarvester.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.cmd; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import org.reactivestreams.Publisher; 6 | import org.reactivestreams.Subscriber; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import io.reactivex.rxjava3.core.Flowable; 12 | 13 | 14 | public class ErrorHarvester extends Harvester { 15 | 16 | public ErrorHarvester(Publisher source, Cmd cmd) { 17 | super(source, cmd); 18 | } 19 | 20 | @Override 21 | public Publisher apply(Flowable upstream) { 22 | return new ErrorHarvester(upstream, cmd); 23 | } 24 | 25 | @Override 26 | protected void subscribeActual(Subscriber actual) { 27 | source.subscribe(new ErrorSub(actual, cmd)); 28 | } 29 | 30 | static class ErrorSub extends BaseSub { 31 | private static final String TAG = Harvester.TAG + ":Error"; 32 | private final Cmd cmd; 33 | 34 | ErrorSub(Subscriber customer, Cmd cmd) { 35 | super(TAG, customer, cmd.isErrorBufferEnabled() ? new ArrayList<>() : null, cmd.getErrorProcessor()); 36 | this.cmd = cmd; 37 | } 38 | 39 | @Override 40 | public boolean parse(String line) { 41 | String contentPart = line; 42 | 43 | final int markerIndex = line.indexOf(cmd.getMarker()); 44 | if (markerIndex == 0) contentPart = null; 45 | else if (markerIndex > 0) contentPart = line.substring(0, markerIndex - 1); 46 | 47 | if (contentPart != null) { 48 | publishParsed(contentPart); 49 | } 50 | 51 | return markerIndex >= 0; 52 | } 53 | 54 | @Override 55 | Crop buildCropHarvest(@Nullable List buffer, boolean isComplete) { 56 | return new Crop(buffer, isComplete); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/cmd/Harvester.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.cmd; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import org.reactivestreams.Publisher; 6 | import org.reactivestreams.Subscriber; 7 | import org.reactivestreams.Subscription; 8 | 9 | import java.io.IOException; 10 | import java.util.List; 11 | 12 | import eu.darken.rxshell.extra.RXSDebug; 13 | import io.reactivex.rxjava3.core.Flowable; 14 | import io.reactivex.rxjava3.core.FlowableTransformer; 15 | import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; 16 | import io.reactivex.rxjava3.processors.FlowableProcessor; 17 | import timber.log.Timber; 18 | 19 | public abstract class Harvester extends Flowable implements FlowableTransformer { 20 | static final String TAG = "RXS:Harvester"; 21 | 22 | final Publisher source; 23 | final Cmd cmd; 24 | 25 | public static class Crop { 26 | @Nullable final List buffer; 27 | final boolean isComplete; 28 | 29 | public Crop(@Nullable List buffer, boolean isComplete) { 30 | this.buffer = buffer; 31 | this.isComplete = isComplete; 32 | } 33 | } 34 | 35 | public Harvester(Publisher source, Cmd cmd) { 36 | this.source = source; 37 | this.cmd = cmd; 38 | } 39 | 40 | static abstract class BaseSub implements Subscriber, Subscription { 41 | private final String tag; 42 | private final Subscriber customer; 43 | private final FlowableProcessor processor; 44 | private final List buffer; 45 | private volatile boolean isDone = false; 46 | Subscription subscription; 47 | 48 | BaseSub(String tag, Subscriber customer, @Nullable List buffer, @Nullable FlowableProcessor processor) { 49 | this.tag = tag; 50 | this.customer = customer; 51 | this.processor = processor; 52 | this.buffer = buffer; 53 | } 54 | 55 | @Override 56 | public void onSubscribe(Subscription subscription) { 57 | if (SubscriptionHelper.validate(this.subscription, subscription)) { 58 | this.subscription = subscription; 59 | customer.onSubscribe(this); 60 | } 61 | } 62 | 63 | abstract boolean parse(String line); 64 | 65 | void publishParsed(String contentPart) { 66 | if (buffer != null) buffer.add(contentPart); 67 | if (processor != null) processor.onNext(contentPart); 68 | } 69 | 70 | abstract T buildCropHarvest(@Nullable List buffer, boolean complete); 71 | 72 | void endHarvest(boolean isComplete) { 73 | if (RXSDebug.isDebug()) Timber.tag(tag).d("endHarvest(isComplete=%b, isDone=%b)", isComplete, isDone); 74 | 75 | if (isDone) return; 76 | isDone = true; 77 | 78 | subscription.cancel(); 79 | 80 | customer.onNext(buildCropHarvest(buffer, isComplete)); 81 | customer.onComplete(); 82 | 83 | if (processor != null) { 84 | if (isComplete) processor.onComplete(); 85 | else processor.onError(new IOException("Upstream completed prematurely.")); 86 | } 87 | } 88 | 89 | @Override 90 | public void onNext(String line) { 91 | if (RXSDebug.isDebug()) Timber.tag(tag).v(line); 92 | if (parse(line)) endHarvest(true); 93 | } 94 | 95 | @Override 96 | public void onError(Throwable e) { 97 | if (RXSDebug.isDebug()) Timber.tag(tag).v("onError(%s)", e.toString()); 98 | endHarvest(false); 99 | } 100 | 101 | @Override 102 | public void onComplete() { 103 | if (RXSDebug.isDebug()) Timber.tag(tag).v("onComplete()"); 104 | endHarvest(false); 105 | } 106 | 107 | @Override 108 | public void request(long n) { 109 | if (RXSDebug.isDebug()) Timber.tag(tag).v("request(%d)", n); 110 | subscription.request(n); 111 | } 112 | 113 | @Override 114 | public void cancel() { 115 | if (RXSDebug.isDebug()) Timber.tag(tag).v("cancel()"); 116 | subscription.cancel(); 117 | } 118 | } 119 | 120 | public static class Factory { 121 | public OutputHarvester forOutput(Publisher source, Cmd cmd) { 122 | return new OutputHarvester(source, cmd); 123 | } 124 | 125 | public ErrorHarvester forError(Publisher source, Cmd cmd) { 126 | return new ErrorHarvester(source, cmd); 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/cmd/OutputHarvester.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.cmd; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import org.reactivestreams.Publisher; 6 | import org.reactivestreams.Subscriber; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import io.reactivex.rxjava3.core.Flowable; 12 | import timber.log.Timber; 13 | 14 | public class OutputHarvester extends Harvester { 15 | public static class Crop extends Harvester.Crop { 16 | final Integer exitCode; 17 | 18 | public Crop(@Nullable List buffer, @Nullable Integer exitCode, boolean isComplete) { 19 | super(buffer, isComplete); 20 | this.exitCode = exitCode; 21 | } 22 | } 23 | 24 | public OutputHarvester(Publisher source, Cmd cmd) { 25 | super(source, cmd); 26 | } 27 | 28 | @Override 29 | protected void subscribeActual(Subscriber actual) { 30 | final OutputSub harvester = new OutputSub(actual, cmd); 31 | source.subscribe(harvester); 32 | } 33 | 34 | @Override 35 | public Publisher apply(Flowable upstream) { 36 | return new OutputHarvester(upstream, cmd); 37 | } 38 | 39 | static class OutputSub extends BaseSub { 40 | private static final String TAG = Harvester.TAG + ":Output"; 41 | private final Cmd cmd; 42 | int exitCode = Cmd.ExitCode.INITIAL; 43 | 44 | OutputSub(Subscriber customer, Cmd cmd) { 45 | super(TAG, customer, cmd.isOutputBufferEnabled() ? new ArrayList<>() : null, cmd.getOutputProcessor()); 46 | this.cmd = cmd; 47 | } 48 | 49 | @Override 50 | public boolean parse(String line) { 51 | String contentPart = line; 52 | String markerPart = null; 53 | 54 | final int markerIndex = line.indexOf(cmd.getMarker()); 55 | if (markerIndex == 0) { 56 | contentPart = null; 57 | markerPart = line; 58 | } else if (markerIndex > 0) { 59 | contentPart = line.substring(0, markerIndex); 60 | markerPart = line.substring(markerIndex); 61 | } 62 | 63 | if (contentPart != null) { 64 | publishParsed(contentPart); 65 | } 66 | 67 | if (markerPart != null) { 68 | try { 69 | exitCode = Integer.valueOf(markerPart.substring(cmd.getMarker().length() + 1), 10); 70 | } catch (Exception e) { 71 | Timber.tag(TAG).e(e); 72 | exitCode = Cmd.ExitCode.EXCEPTION; 73 | } 74 | return true; 75 | } else { 76 | return false; 77 | } 78 | } 79 | 80 | @Override 81 | Crop buildCropHarvest(@Nullable List buffer, boolean isComplete) { 82 | return new Crop(buffer, exitCode, isComplete); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/extra/ApiWrap.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.extra; 2 | 3 | import android.os.Build; 4 | 5 | public class ApiWrap { 6 | static int SDK_INT = Build.VERSION.SDK_INT; 7 | 8 | public static int getCurrentSDKInt() { 9 | return SDK_INT; 10 | } 11 | 12 | public static void setSDKInt(int sdkInt) { 13 | SDK_INT = sdkInt; 14 | } 15 | 16 | /** 17 | * @return if >=17 18 | */ 19 | public static boolean hasJellyBeanMR1() { 20 | return getCurrentSDKInt() >= Build.VERSION_CODES.JELLY_BEAN_MR1; 21 | } 22 | 23 | /** 24 | * @return if >=18 25 | */ 26 | public static boolean hasJellyBeanMR2() { 27 | return getCurrentSDKInt() >= Build.VERSION_CODES.JELLY_BEAN_MR2; 28 | } 29 | 30 | /** 31 | * @return if >=19 32 | */ 33 | public static boolean hasKitKat() { 34 | return getCurrentSDKInt() >= Build.VERSION_CODES.KITKAT; 35 | } 36 | 37 | /** 38 | * @return if >=26 39 | */ 40 | public static boolean hasOreo() { 41 | return getCurrentSDKInt() >= Build.VERSION_CODES.O; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/extra/CmdHelper.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.extra; 2 | 3 | 4 | import android.support.annotation.Nullable; 5 | 6 | public class CmdHelper { 7 | /** 8 | * Sanitize command line input 9 | */ 10 | @Nullable 11 | public static String san(@Nullable String input) { 12 | if (input == null) return null; 13 | return "'" + input.replace("'", "'\\''") + "'"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/extra/EnvVar.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.extra; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | 6 | public class EnvVar { 7 | public final @Nullable F first; 8 | public final @Nullable S second; 9 | 10 | public EnvVar(@Nullable F first, @Nullable S second) { 11 | this.first = first; 12 | this.second = second; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | return "EnvVar{" + String.valueOf(first) + " " + String.valueOf(second) + "}"; 18 | } 19 | 20 | @NonNull 21 | public static EnvVar create(@Nullable A a, @Nullable B b) { 22 | return new EnvVar<>(a, b); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/extra/HasEnvironmentVariables.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.extra; 2 | 3 | import java.util.Collection; 4 | 5 | public interface HasEnvironmentVariables { 6 | 7 | Collection> getEnvironmentVariables(boolean root); 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/extra/RXSDebug.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.extra; 2 | 3 | 4 | import android.support.annotation.VisibleForTesting; 5 | 6 | import java.util.Collections; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | 10 | import eu.darken.rxshell.BuildConfig; 11 | import timber.log.Timber; 12 | 13 | public class RXSDebug { 14 | private static final String TAG = "RXS:Debug"; 15 | private static boolean DEBUG = BuildConfig.DEBUG; 16 | 17 | public static void setDebug(boolean debug) { 18 | Timber.tag(TAG).i("setDebug(debug=%b)", debug); 19 | DEBUG = debug; 20 | } 21 | 22 | public static boolean isDebug() { 23 | return DEBUG; 24 | } 25 | 26 | @VisibleForTesting final static Set CALLBACKS = Collections.synchronizedSet(new HashSet<>()); 27 | 28 | private static Set getProcessCallbacks() { 29 | if (CALLBACKS.isEmpty()) return Collections.emptySet(); 30 | Set callbacks; 31 | synchronized (CALLBACKS) { 32 | callbacks = new HashSet<>(); 33 | for (Callback callback : CALLBACKS) { 34 | if (callback instanceof ProcessCallback) { 35 | callbacks.add((ProcessCallback) callback); 36 | } 37 | } 38 | } 39 | return callbacks; 40 | } 41 | 42 | public static void notifyOnProcessStart(Process process) { 43 | for (ProcessCallback c : getProcessCallbacks()) { 44 | c.onProcessStart(process); 45 | } 46 | } 47 | 48 | public static void notifyOnProcessEnd(Process process) { 49 | for (ProcessCallback c : getProcessCallbacks()) { 50 | c.onProcessEnd(process); 51 | } 52 | } 53 | 54 | public interface Callback { 55 | 56 | } 57 | 58 | public interface ProcessCallback extends Callback { 59 | void onProcessStart(Process process); 60 | 61 | void onProcessEnd(Process process); 62 | } 63 | 64 | public static void addCallback(Callback callback) { 65 | CALLBACKS.add(callback); 66 | } 67 | 68 | public static void removeCallback(Callback callback) { 69 | CALLBACKS.remove(callback); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/extra/RxCmdShellHelper.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.extra; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import java.io.IOException; 6 | 7 | import eu.darken.rxshell.cmd.RxCmdShell; 8 | 9 | 10 | public class RxCmdShellHelper { 11 | 12 | public static RxCmdShell.Session blockingOpen(RxCmdShell shell) throws IOException { 13 | try { 14 | return shell.open().blockingGet(); 15 | } catch (RuntimeException e) { 16 | if (e.getCause() instanceof IOException) { 17 | throw (IOException) e.getCause(); 18 | } else throw e; 19 | } 20 | } 21 | 22 | public static RxCmdShell.Session blockingOpen(RxCmdShell.Builder builder) throws IOException { 23 | return blockingOpen(builder.build()); 24 | } 25 | 26 | public static void blockingCancel(@Nullable RxCmdShell.Session session) throws IOException { 27 | if (session == null) return; 28 | try { 29 | session.cancel().blockingAwait(); 30 | } catch (RuntimeException e) { 31 | if (e.getCause() instanceof IOException) { 32 | throw (IOException) e.getCause(); 33 | } else throw e; 34 | } 35 | } 36 | 37 | public static Integer blockingClose(@Nullable RxCmdShell.Session session) throws IOException { 38 | if (session == null) return -1; 39 | try { 40 | return session.close().blockingGet(); 41 | } catch (RuntimeException e) { 42 | if (e.getCause() instanceof IOException) { 43 | throw (IOException) e.getCause(); 44 | } else throw e; 45 | } 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/process/DefaultProcessFactory.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | 4 | import java.io.IOException; 5 | 6 | public class DefaultProcessFactory implements ProcessFactory { 7 | @Override 8 | public Process start(String... commands) throws IOException { 9 | return new ProcessBuilder(commands).start(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/process/ProcessFactory.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | 4 | import java.io.IOException; 5 | 6 | public interface ProcessFactory { 7 | Process start(String... commands) throws IOException; 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/process/ProcessHelper.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import eu.darken.rxshell.extra.ApiWrap; 6 | 7 | 8 | public class ProcessHelper { 9 | @SuppressLint("NewApi") 10 | public static boolean isAlive(Process process) { 11 | if (ApiWrap.hasOreo()) { 12 | return process.isAlive(); 13 | } else { 14 | try { 15 | process.exitValue(); 16 | return false; 17 | } catch (IllegalThreadStateException e) { 18 | return true; 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/process/ProcessKiller.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | public interface ProcessKiller { 4 | 5 | boolean kill(Process process); 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/process/RootKiller.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.io.OutputStreamWriter; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | 15 | import eu.darken.rxshell.extra.RXSDebug; 16 | import eu.darken.rxshell.shell.LineReader; 17 | import io.reactivex.rxjava3.core.Observable; 18 | import io.reactivex.rxjava3.core.ObservableOnSubscribe; 19 | import io.reactivex.rxjava3.core.Single; 20 | import io.reactivex.rxjava3.schedulers.Schedulers; 21 | import timber.log.Timber; 22 | 23 | public class RootKiller implements ProcessKiller { 24 | private static final String TAG = "RXS:RootKiller"; 25 | private static final Pattern PID_PATTERN = Pattern.compile("^.+?pid=(\\d+).+?$"); 26 | private static final Pattern SPACES_PATTERN = Pattern.compile("\\s+"); 27 | private final ProcessFactory processFactory; 28 | 29 | public RootKiller(ProcessFactory processFactory) { 30 | this.processFactory = processFactory; 31 | } 32 | 33 | public boolean kill(Process process) { 34 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("kill(%s)", process); 35 | if (!ProcessHelper.isAlive(process)) { 36 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("Process is no longer alive, skipping kill."); 37 | return true; 38 | } 39 | // stupid method for getting the pid, but it actually works 40 | Matcher matcher = PID_PATTERN.matcher(process.toString()); 41 | if (!matcher.matches()) { 42 | if (RXSDebug.isDebug()) Timber.tag(TAG).e("Can't find PID for %s", process); 43 | return false; 44 | } 45 | int pid = Integer.parseInt(matcher.group(1)); 46 | List allRelatedPids = getAllPids(pid); 47 | 48 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("Related pids: %s", allRelatedPids); 49 | 50 | if (allRelatedPids != null && destroyPids(allRelatedPids)) { 51 | return true; 52 | } else { 53 | if (RXSDebug.isDebug()) Timber.tag(TAG).w("Couldn't destroy process via root shell, trying Process.destroy()"); 54 | process.destroy(); 55 | return false; 56 | } 57 | } 58 | 59 | static Single> makeMiniHarvester(InputStream inputStream) { 60 | return Observable 61 | .create((ObservableOnSubscribe) emitter -> { 62 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 63 | LineReader lineReader = new LineReader(); 64 | String line; 65 | try { 66 | while ((line = lineReader.readLine(reader)) != null && !emitter.isDisposed()) { 67 | emitter.onNext(line); 68 | } 69 | } catch (IOException e) { 70 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("MiniHarvester read error: %s", e.getMessage()); 71 | } finally { 72 | emitter.onComplete(); 73 | } 74 | }) 75 | .doOnEach(n -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v("miniHarvesters:doOnEach %s", n); }) 76 | .subscribeOn(Schedulers.io()) 77 | .toList() 78 | .onErrorReturnItem(new ArrayList<>()) 79 | .cache(); 80 | } 81 | 82 | // use 'ps' to get this pid and all pids that are related to it (e.g. spawned by it) 83 | @Nullable 84 | private List getAllPids(final int parentPid) { 85 | final List result = new ArrayList<>(); 86 | result.add(parentPid); 87 | Process process = null; 88 | try { 89 | process = processFactory.start("su"); 90 | OutputStreamWriter os = new OutputStreamWriter(process.getOutputStream()); 91 | 92 | Single> errorsHarvester = makeMiniHarvester(process.getErrorStream()); 93 | errorsHarvester.subscribe(); 94 | 95 | Single> outputHarvester = makeMiniHarvester(process.getInputStream()); 96 | outputHarvester.subscribe(); 97 | 98 | os.write("ps" + LineReader.getLineSeparator()); 99 | os.write("exit" + LineReader.getLineSeparator()); 100 | os.flush(); 101 | os.close(); 102 | 103 | int exitcode = process.waitFor(); 104 | 105 | errorsHarvester.blockingGet(); 106 | final List output = outputHarvester.blockingGet(); 107 | 108 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("getAllPids() exitcode: %d", exitcode); 109 | 110 | if (exitcode == RxProcess.ExitCode.OK) { 111 | for (String s : output) { 112 | if (output.indexOf(s) == 0) continue; // SKIP title row 113 | String[] line = SPACES_PATTERN.split(s); 114 | if (line.length >= 3) { 115 | try { 116 | if (parentPid == Integer.parseInt(line[2])) result.add(Integer.parseInt(line[1])); 117 | } catch (NumberFormatException e) { 118 | Timber.tag(TAG).w(e, "getAllPids(parentPid=%d) parse failure: %s", parentPid, line); 119 | } 120 | } 121 | } 122 | } 123 | 124 | } catch (InterruptedException interrupt) { 125 | Timber.tag(TAG).w(interrupt, "Interrupted!"); 126 | return null; 127 | } catch (IOException e) { 128 | Timber.tag(TAG).w(e, "IOException, IOException, pipe broke?"); 129 | return null; 130 | } finally { 131 | if (process != null) process.destroy(); 132 | } 133 | return result; 134 | } 135 | 136 | private boolean destroyPids(List pids) { 137 | Process process = null; 138 | try { 139 | process = processFactory.start("su"); 140 | 141 | OutputStreamWriter outputStream = new OutputStreamWriter(process.getOutputStream()); 142 | 143 | makeMiniHarvester(process.getErrorStream()).subscribe(); 144 | makeMiniHarvester(process.getInputStream()).subscribe(); 145 | 146 | for (Integer p : pids) outputStream.write("kill " + p + LineReader.getLineSeparator()); 147 | 148 | outputStream.write("exit" + LineReader.getLineSeparator()); 149 | outputStream.flush(); 150 | outputStream.close(); 151 | 152 | int exitcode = process.waitFor(); 153 | 154 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("destroyPids(pids=%s) exitcode: %d", pids, exitcode); 155 | return exitcode == RxProcess.ExitCode.OK; 156 | } catch (InterruptedException interrupt) { 157 | Timber.tag(TAG).w("destroyPids(pids=%s) Interrupted!", pids); 158 | return false; 159 | } catch (IOException e) { 160 | Timber.tag(TAG).w("destroyPids(pids=%s) IOException, command failed? not found?", pids); 161 | return false; 162 | } finally { 163 | if (process != null) process.destroy(); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/process/RxProcess.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.lang.ref.WeakReference; 8 | 9 | import eu.darken.rxshell.extra.ApiWrap; 10 | import eu.darken.rxshell.extra.RXSDebug; 11 | import io.reactivex.rxjava3.core.Completable; 12 | import io.reactivex.rxjava3.core.Observable; 13 | import io.reactivex.rxjava3.core.Observer; 14 | import io.reactivex.rxjava3.core.Single; 15 | import io.reactivex.rxjava3.core.SingleEmitter; 16 | import io.reactivex.rxjava3.core.SingleOnSubscribe; 17 | import io.reactivex.rxjava3.disposables.Disposable; 18 | import io.reactivex.rxjava3.functions.Action; 19 | import io.reactivex.rxjava3.schedulers.Schedulers; 20 | import timber.log.Timber; 21 | 22 | 23 | public class RxProcess { 24 | public static class ExitCode { 25 | public static final int OK = 0; 26 | public static final int PROBLEM = 1; 27 | public static final int OUTOFRANGE = 255; 28 | } 29 | 30 | private static final String TAG = "RXS:RxProcess"; 31 | private final Observable processCreator; 32 | private Single session; 33 | 34 | public RxProcess(ProcessFactory processFactory, ProcessKiller processKiller, String... commands) { 35 | this.processCreator = Observable.create(e -> { 36 | final Process process = processFactory.start(commands); 37 | e.setCancellable(() -> { 38 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("cancel()"); 39 | processKiller.kill(process); 40 | }); 41 | e.onNext(process); 42 | process.waitFor(); 43 | e.onComplete(); 44 | }); 45 | } 46 | 47 | public synchronized Single open() { 48 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("open()"); 49 | if (session == null) { 50 | this.session = Single 51 | .create(new SingleOnSubscribe() { 52 | WeakReference debugRef; 53 | 54 | @Override 55 | public void subscribe(SingleEmitter emitter) { 56 | processCreator 57 | .doFinally((Action) () -> { 58 | synchronized (RxProcess.this) { 59 | RXSDebug.notifyOnProcessEnd(debugRef != null ? debugRef.get() : null); 60 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("Process finished, clearing session"); 61 | session = null; 62 | } 63 | }) 64 | .subscribe(new Observer() { 65 | Disposable disposable; 66 | 67 | @Override 68 | public void onSubscribe(Disposable disposable) { 69 | this.disposable = disposable; 70 | } 71 | 72 | @Override 73 | public void onNext(Process process) { 74 | debugRef = new WeakReference<>(process); 75 | RXSDebug.notifyOnProcessStart(process); 76 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("processCreator:onNext(%s)", process); 77 | emitter.onSuccess(new Session(process, disposable)); 78 | } 79 | 80 | @Override 81 | public void onError(Throwable e) { 82 | if (RXSDebug.isDebug()) Timber.tag(TAG).v(e, "processCreator:onError()"); 83 | emitter.tryOnError(e); 84 | } 85 | 86 | @Override 87 | public void onComplete() { 88 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("processCreator:onComplete()"); 89 | disposable.dispose(); 90 | } 91 | }); 92 | } 93 | }) 94 | .subscribeOn(Schedulers.io()) 95 | .doOnSuccess(s -> { if (RXSDebug.isDebug()) Timber.tag(TAG).d("open():doOnSuccess %s", s);}) 96 | .doOnError(t -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v(t, "open():doOnError");}) 97 | .cache(); 98 | } 99 | return session; 100 | } 101 | 102 | public synchronized Single isAlive() { 103 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("isAlive()"); 104 | if (session == null) return Single.just(false); 105 | else return session.flatMap(Session::isAlive); 106 | } 107 | 108 | public synchronized Completable close() { 109 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("close()"); 110 | if (session == null) return Completable.complete(); 111 | else return session.flatMapCompletable(s -> s.destroy().andThen(s.waitFor().ignoreElement())); 112 | } 113 | 114 | public static class Session { 115 | private static final String TAG = RxProcess.TAG + ":Session"; 116 | final Process process; 117 | private final Single waitFor; 118 | private final Completable destroy; 119 | 120 | public Session(Process process, Disposable processDisposable) { 121 | this.process = process; 122 | this.destroy = Completable 123 | .create(e -> { 124 | processDisposable.dispose(); 125 | e.onComplete(); 126 | }) 127 | .subscribeOn(Schedulers.io()) 128 | .doOnComplete(() -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v("destroy():doOnComplete");}) 129 | .doOnError(t -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v(t, "destroy():doOnError");}) 130 | .cache(); 131 | this.waitFor = Single 132 | .create((SingleOnSubscribe) e -> { 133 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("Waiting for %s to exit.", process); 134 | int exitCode = process.waitFor(); 135 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("Exitcode: %d, Process: %s", exitCode, process); 136 | e.onSuccess(exitCode); 137 | }) 138 | .subscribeOn(Schedulers.io()) 139 | .doOnSuccess(s -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v("waitFor():doOnSuccess %s", s);}) 140 | .doOnError(t -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v(t, "waitFor():doOnError");}) 141 | .cache(); 142 | } 143 | 144 | @SuppressLint("NewApi") 145 | public Single isAlive() { 146 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("isAlive()"); 147 | return Single 148 | .create((SingleEmitter emitter) -> { 149 | if (ApiWrap.hasOreo()) { 150 | emitter.onSuccess(process.isAlive()); 151 | } else { 152 | try { 153 | process.exitValue(); 154 | emitter.onSuccess(false); 155 | } catch (IllegalThreadStateException e) { 156 | emitter.onSuccess(true); 157 | } 158 | } 159 | }) 160 | .subscribeOn(Schedulers.io()); 161 | } 162 | 163 | public Single waitFor() { 164 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("waitFor()"); 165 | return waitFor; 166 | } 167 | 168 | public Completable destroy() { 169 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("destroy()"); 170 | return destroy; 171 | } 172 | 173 | public OutputStream input() { 174 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("input()"); 175 | return process.getOutputStream(); 176 | } 177 | 178 | public InputStream output() { 179 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("output()"); 180 | return process.getInputStream(); 181 | } 182 | 183 | public InputStream error() { 184 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("error()"); 185 | return process.getErrorStream(); 186 | } 187 | 188 | @Override 189 | public String toString() { 190 | return "RxProcess.Session(process=" + process + ")"; 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/process/UserKiller.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import eu.darken.rxshell.extra.ApiWrap; 6 | import eu.darken.rxshell.extra.RXSDebug; 7 | import timber.log.Timber; 8 | 9 | 10 | public class UserKiller implements ProcessKiller { 11 | private static final String TAG = "RXS:UserKiller"; 12 | 13 | @SuppressLint("NewApi") 14 | @Override 15 | public boolean kill(Process process) { 16 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("kill(%s)", process); 17 | if (!ProcessHelper.isAlive(process)) { 18 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("Process is no longer alive, skipping kill."); 19 | return true; 20 | } 21 | 22 | if (ApiWrap.hasOreo()) process.destroyForcibly(); 23 | else process.destroy(); 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/shell/LineReader.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.shell; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import java.io.IOException; 6 | import java.io.Reader; 7 | 8 | import eu.darken.rxshell.extra.ApiWrap; 9 | 10 | 11 | public class LineReader { 12 | private final char[] lineSeparator; 13 | 14 | public LineReader() { 15 | lineSeparator = getLineSeparator().toCharArray(); 16 | } 17 | 18 | public LineReader(String lineSeparator) { 19 | this.lineSeparator = lineSeparator.toCharArray(); 20 | } 21 | 22 | @SuppressLint("NewApi") 23 | public static String getLineSeparator() { 24 | if (ApiWrap.hasKitKat()) return System.lineSeparator(); 25 | else return System.getProperty("line.separator", "\n"); 26 | } 27 | 28 | public String readLine(Reader reader) throws IOException { 29 | char curChar; 30 | int val; 31 | StringBuilder sb = new StringBuilder(40); 32 | while ((val = reader.read()) != -1) { 33 | curChar = (char) val; 34 | if (curChar == '\n' && lineSeparator.length == 1 && curChar == lineSeparator[0]) { 35 | return sb.toString(); 36 | } else if (curChar == '\r' && lineSeparator.length == 1 && curChar == lineSeparator[0]) { 37 | return sb.toString(); 38 | } else if (curChar == '\n' && lineSeparator.length == 2 && curChar == lineSeparator[1]) { 39 | if (sb.length() > 0 && sb.charAt(sb.length() - 1) == lineSeparator[0]) { 40 | sb.deleteCharAt(sb.length() - 1); 41 | return sb.toString(); 42 | } 43 | } 44 | sb.append(curChar); 45 | } 46 | if (sb.length() == 0) return null; 47 | return sb.toString(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/src/main/java/eu/darken/rxshell/shell/RxShell.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.shell; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStreamWriter; 8 | import java.nio.charset.StandardCharsets; 9 | 10 | import eu.darken.rxshell.extra.RXSDebug; 11 | import eu.darken.rxshell.process.RxProcess; 12 | import io.reactivex.rxjava3.core.BackpressureStrategy; 13 | import io.reactivex.rxjava3.core.Completable; 14 | import io.reactivex.rxjava3.core.Flowable; 15 | import io.reactivex.rxjava3.core.FlowableEmitter; 16 | import io.reactivex.rxjava3.core.Single; 17 | import io.reactivex.rxjava3.disposables.Disposable; 18 | import io.reactivex.rxjava3.schedulers.Schedulers; 19 | import timber.log.Timber; 20 | 21 | public class RxShell { 22 | private static final String TAG = "RXS:RxShell"; 23 | private RxProcess rxProcess; 24 | private Single session; 25 | 26 | public RxShell(RxProcess rxProcess) { 27 | this.rxProcess = rxProcess; 28 | } 29 | 30 | public synchronized Single open() { 31 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("open()"); 32 | if (session == null) { 33 | session = rxProcess.open() 34 | .map(session -> { 35 | OutputStreamWriter writer = new OutputStreamWriter(session.input(), StandardCharsets.UTF_8); 36 | return new Session(session, writer); 37 | }) 38 | .subscribeOn(Schedulers.io()) 39 | .doOnSuccess(s -> { 40 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("open():doOnSuccess %s", s); 41 | s.waitFor().subscribe(integer -> { 42 | synchronized (RxShell.this) { 43 | session = null; 44 | } 45 | }, e -> Timber.tag(TAG).w(e, "Error resetting session.")); 46 | }) 47 | .doOnError(t -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v(t, "open():doOnError");}) 48 | .cache(); 49 | } 50 | return session; 51 | } 52 | 53 | public synchronized Single isAlive() { 54 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("isAlive()"); 55 | if (session == null) return Single.just(false); 56 | else return session.flatMap(Session::isAlive); 57 | } 58 | 59 | public synchronized Completable cancel() { 60 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("cancel()"); 61 | if (session == null) return Completable.complete(); 62 | else return session.flatMapCompletable(Session::cancel); 63 | } 64 | 65 | public synchronized Single close() { 66 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("close()"); 67 | if (session == null) return Single.just(0); 68 | else return session.flatMap(Session::close); 69 | } 70 | 71 | public static class Session { 72 | private static final String TAG = RxShell.TAG + ":Session"; 73 | private final RxProcess.Session processSession; 74 | private final OutputStreamWriter writer; 75 | private final Flowable outputLines; 76 | private final Flowable errorLines; 77 | private final Single close; 78 | private final Single waitFor; 79 | private final Disposable errorKeepAlive; 80 | private final Disposable outputKeepAlive; 81 | private final Completable cancel; 82 | 83 | public Session(RxProcess.Session processSession, OutputStreamWriter writer) { 84 | this.processSession = processSession; 85 | this.writer = writer; 86 | 87 | this.outputLines = makeLineStream(processSession.output(), "output"); 88 | this.outputKeepAlive = this.outputLines.subscribe(s -> { }, t -> Timber.w(t, "OutputLines KeepAlive")); 89 | 90 | this.errorLines = makeLineStream(processSession.error(), "error"); 91 | this.errorKeepAlive = this.errorLines().subscribe(s -> { }, t -> Timber.w("ErrorLines KeepAlive")); 92 | 93 | this.cancel = processSession.destroy() 94 | .doOnComplete(() -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v("cancel():doOnComplete");}) 95 | .doOnError(t -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v(t, "cancel():doOnError");}) 96 | .cache(); 97 | this.waitFor = processSession.waitFor() 98 | .doOnSuccess(s -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v("waitFor():doOnSuccess %s", s);}) 99 | .doOnError(t -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v(t, "waitFor():doOnError");}) 100 | .cache(); 101 | this.close = Completable 102 | .create(emitter -> { 103 | try { 104 | writeLine("exit", true); 105 | writer.close(); 106 | } catch (IOException e) { 107 | if (RXSDebug.isDebug()) 108 | Timber.tag(TAG).v("Trying to close output, but it's already closed: %s", e.getMessage()); 109 | } finally { 110 | emitter.onComplete(); 111 | } 112 | }) 113 | .subscribeOn(Schedulers.io()) 114 | .andThen(waitFor()) 115 | .doFinally(() -> { 116 | outputKeepAlive.dispose(); 117 | errorKeepAlive.dispose(); 118 | }) 119 | .doOnSuccess(s -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v("close():doOnSuccess %s", s);}) 120 | .doOnError(t -> { if (RXSDebug.isDebug()) Timber.tag(TAG).v(t, "close():doOnError");}) 121 | .cache(); 122 | } 123 | 124 | public void writeLine(String line, boolean flush) throws IOException { 125 | if (RXSDebug.isDebug()) Timber.tag(TAG).d("writeLine(line=%s, flush=%b)", line, flush); 126 | writer.write(line + LineReader.getLineSeparator()); 127 | if (flush) writer.flush(); 128 | } 129 | 130 | public Single isAlive() { 131 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("isAlive()"); 132 | return processSession.isAlive(); 133 | } 134 | 135 | public Completable cancel() { 136 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("cancel()"); 137 | return cancel; 138 | } 139 | 140 | public Single waitFor() { 141 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("waitFor()"); 142 | return waitFor; 143 | } 144 | 145 | public Single close() { 146 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("close()"); 147 | return close; 148 | } 149 | 150 | public Flowable outputLines() { 151 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("outputLines()"); 152 | return outputLines; 153 | } 154 | 155 | public Flowable errorLines() { 156 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("errorLines()"); 157 | return errorLines; 158 | } 159 | 160 | @Override 161 | public String toString() { 162 | return "RxShell.Session(processSession=" + processSession + ")"; 163 | } 164 | } 165 | 166 | static Flowable makeLineStream(InputStream stream, String tag) { 167 | return Flowable 168 | .create((FlowableEmitter emitter) -> { 169 | final InputStreamReader inputStreamReader = new InputStreamReader(stream, StandardCharsets.UTF_8); 170 | final BufferedReader reader = new BufferedReader(inputStreamReader); 171 | emitter.setCancellable(() -> { 172 | try { 173 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("LineStream:%s onCancel()", tag); 174 | // https://stackoverflow.com/questions/3595926/how-to-interrupt-bufferedreaders-readline 175 | stream.close(); 176 | reader.close(); 177 | } catch (IOException e) { 178 | if (RXSDebug.isDebug()) Timber.tag(TAG).w("LineStream:%s Cancel error: %s", tag, e.getMessage()); 179 | } 180 | }); 181 | LineReader lineReader = new LineReader(); 182 | String line; 183 | try { 184 | while ((line = lineReader.readLine(reader)) != null && !emitter.isCancelled()) { 185 | emitter.onNext(line); 186 | } 187 | } catch (IOException e) { 188 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("LineStream:%s Read error: %s", tag, e.getMessage()); 189 | } finally { 190 | if (RXSDebug.isDebug()) Timber.tag(TAG).v("LineStream:%s onComplete()", tag); 191 | emitter.onComplete(); 192 | } 193 | }, BackpressureStrategy.MISSING) 194 | .subscribeOn(Schedulers.io()) 195 | .share(); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /core/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/cmd/CmdBuilderTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.cmd; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.junit.MockitoJUnitRunner; 6 | 7 | import java.io.IOException; 8 | import java.util.Arrays; 9 | import java.util.UUID; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import io.reactivex.rxjava3.core.Single; 13 | import io.reactivex.rxjava3.processors.PublishProcessor; 14 | import io.reactivex.rxjava3.subscribers.TestSubscriber; 15 | import testtools.BaseTest; 16 | 17 | import static junit.framework.Assert.assertEquals; 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.contains; 20 | import static org.hamcrest.Matchers.nullValue; 21 | import static org.hamcrest.core.Is.is; 22 | import static org.hamcrest.core.IsNot.not; 23 | import static org.mockito.ArgumentMatchers.any; 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.verify; 26 | import static org.mockito.Mockito.when; 27 | 28 | @RunWith(MockitoJUnitRunner.class) 29 | public class CmdBuilderTest extends BaseTest { 30 | 31 | @Test 32 | public void testBuilder_from() { 33 | Cmd orig = Cmd.builder(UUID.randomUUID().toString()) 34 | .outputBuffer(false) 35 | .errorBuffer(false) 36 | .timeout(1337) 37 | .outputProcessor(PublishProcessor.create()) 38 | .errorProcessor(PublishProcessor.create()) 39 | .build(); 40 | 41 | Cmd copy = Cmd.from(orig).build(); 42 | assertEquals(orig.getCommands(), copy.getCommands()); 43 | assertEquals(orig.isOutputBufferEnabled(), copy.isOutputBufferEnabled()); 44 | assertEquals(orig.isErrorBufferEnabled(), copy.isErrorBufferEnabled()); 45 | assertEquals(orig.getTimeout(), copy.getTimeout()); 46 | assertEquals(orig.getOutputProcessor(), copy.getOutputProcessor()); 47 | assertEquals(orig.getErrorProcessor(), copy.getErrorProcessor()); 48 | } 49 | 50 | @Test(expected = IllegalArgumentException.class) 51 | public void testBuilder_empty() { 52 | Cmd.builder().build(); 53 | } 54 | 55 | @Test 56 | public void testBuilder_type1() { 57 | Cmd cmd = Cmd.builder("cmd1", "cmd2").build(); 58 | assertThat(cmd.getCommands(), contains("cmd1", "cmd2")); 59 | } 60 | 61 | @Test 62 | public void testBuilder_type2() { 63 | Cmd cmd = Cmd.builder(Arrays.asList("cmd1", "cmd2")).build(); 64 | assertThat(cmd.getCommands(), contains("cmd1", "cmd2")); 65 | } 66 | 67 | @Test 68 | public void testBuild() { 69 | final PublishProcessor outputPub = PublishProcessor.create(); 70 | final PublishProcessor errorPub = PublishProcessor.create(); 71 | Cmd cmd = Cmd.builder("cmd1") 72 | .outputBuffer(false) 73 | .errorBuffer(false) 74 | .timeout(1337) 75 | .outputProcessor(outputPub) 76 | .errorProcessor(errorPub) 77 | .build(); 78 | assertThat(cmd.getCommands(), contains("cmd1")); 79 | assertThat(cmd.getOutputProcessor(), is(outputPub)); 80 | assertThat(cmd.getErrorProcessor(), is(errorPub)); 81 | assertThat(cmd.getTimeout(), is(1337L)); 82 | assertThat(cmd.isOutputBufferEnabled(), is(false)); 83 | assertThat(cmd.isErrorBufferEnabled(), is(false)); 84 | } 85 | 86 | @Test 87 | public void testBuild_buffer_on_by_default() { 88 | Cmd cmd = Cmd.builder("k").build(); 89 | assertThat(cmd.isOutputBufferEnabled(), is(true)); 90 | assertThat(cmd.isErrorBufferEnabled(), is(true)); 91 | } 92 | 93 | @Test 94 | public void testSubmit() { 95 | RxCmdShell.Session session = mock(RxCmdShell.Session.class); 96 | when(session.submit(any())).thenReturn(Single.just(new Cmd.Result(null))); 97 | 98 | Cmd.builder("").submit(session).blockingGet(); 99 | 100 | verify(session).submit(any()); 101 | } 102 | 103 | @Test 104 | public void testSubmit_oneshot() { 105 | RxCmdShell shell = mock(RxCmdShell.class); 106 | RxCmdShell.Session session = mock(RxCmdShell.Session.class); 107 | when(shell.open()).thenReturn(Single.just(session)); 108 | when(shell.isAlive()).thenReturn(Single.just(false)); 109 | when(session.submit(any())).thenReturn(Single.just(new Cmd.Result(null))); 110 | when(session.close()).thenReturn(Single.just(0)); 111 | 112 | Cmd.builder("").submit(shell).blockingGet(); 113 | 114 | verify(shell).isAlive(); 115 | verify(shell).open(); 116 | verify(session).submit(any()); 117 | verify(session).close(); 118 | } 119 | 120 | @Test 121 | public void testSubmit_oneshot_exception() throws IOException { 122 | RxCmdShell shell = mock(RxCmdShell.class); 123 | when(shell.open()).thenReturn(Single.error(new IOException())); 124 | when(shell.isAlive()).thenReturn(Single.just(false)); 125 | 126 | Cmd.builder("").submit(shell).test().awaitDone(1, TimeUnit.SECONDS).assertError(IOException.class); 127 | } 128 | 129 | @Test 130 | public void testExecute() { 131 | RxCmdShell.Session session = mock(RxCmdShell.Session.class); 132 | when(session.submit(any())).thenReturn(Single.just(new Cmd.Result(null))); 133 | 134 | Cmd.builder("").execute(session); 135 | 136 | verify(session).submit(any()); 137 | } 138 | 139 | @Test 140 | public void testExecute_oneshot() { 141 | RxCmdShell shell = mock(RxCmdShell.class); 142 | RxCmdShell.Session session = mock(RxCmdShell.Session.class); 143 | when(shell.open()).thenReturn(Single.just(session)); 144 | when(shell.isAlive()).thenReturn(Single.just(false)); 145 | when(session.submit(any())).thenReturn(Single.just(new Cmd.Result(null))); 146 | when(session.close()).thenReturn(Single.just(0)); 147 | 148 | Cmd.builder("").execute(shell); 149 | 150 | verify(shell).isAlive(); 151 | verify(shell).open(); 152 | verify(session).submit(any()); 153 | verify(session).close(); 154 | } 155 | 156 | @Test 157 | public void testExecute_oneshot_exception() throws IOException { 158 | RxCmdShell shell = mock(RxCmdShell.class); 159 | Exception ex = new IOException("Error message"); 160 | when(shell.open()).thenReturn(Single.error(ex)); 161 | when(shell.isAlive()).thenReturn(Single.just(false)); 162 | 163 | final Cmd.Result result = Cmd.builder("").execute(shell); 164 | assertThat(result.getExitCode(), is(Cmd.ExitCode.EXCEPTION)); 165 | assertThat(result.getErrors(), contains(ex.toString())); 166 | assertThat(result.getOutput(), is(not(nullValue()))); 167 | } 168 | 169 | @Test 170 | public void testExecute_oneshot_exception_no_buffers() throws IOException { 171 | RxCmdShell shell = mock(RxCmdShell.class); 172 | Exception ex = new IOException("Error message"); 173 | when(shell.open()).thenReturn(Single.error(ex)); 174 | when(shell.isAlive()).thenReturn(Single.just(false)); 175 | 176 | final PublishProcessor errorPub = PublishProcessor.create(); 177 | final TestSubscriber errorSub = errorPub.test(); 178 | final PublishProcessor outputPub = PublishProcessor.create(); 179 | final TestSubscriber outputSub = outputPub.test(); 180 | final Cmd.Result result = Cmd.builder("") 181 | .outputBuffer(false) 182 | .errorBuffer(false) 183 | .outputProcessor(outputPub) 184 | .errorProcessor(errorPub) 185 | .execute(shell); 186 | assertThat(result.getExitCode(), is(Cmd.ExitCode.EXCEPTION)); 187 | assertThat(result.getErrors(), is(nullValue())); 188 | assertThat(result.getOutput(), is(nullValue())); 189 | errorSub.assertValueCount(1); 190 | outputSub.assertValueCount(0); 191 | errorSub.assertComplete(); 192 | outputSub.assertComplete(); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/cmd/CmdResultTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.cmd; 2 | 3 | import org.junit.Test; 4 | import org.mockito.Mock; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import testtools.BaseTest; 11 | 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.contains; 14 | import static org.hamcrest.Matchers.not; 15 | import static org.hamcrest.Matchers.nullValue; 16 | import static org.hamcrest.core.Is.is; 17 | 18 | 19 | public class CmdResultTest extends BaseTest { 20 | @Mock Cmd cmd; 21 | 22 | @Test 23 | public void testConstructor1() { 24 | Cmd.Result result = new Cmd.Result(cmd); 25 | assertThat(result.getExitCode(), is(Cmd.ExitCode.INITIAL)); 26 | assertThat(result.getCmd(), is(cmd)); 27 | assertThat(result.getOutput(), is(not(nullValue()))); 28 | assertThat(result.getErrors(), is(not(nullValue()))); 29 | } 30 | 31 | @Test 32 | public void testConstructor2() { 33 | Cmd.Result result = new Cmd.Result(cmd, Cmd.ExitCode.SHELL_DIED); 34 | assertThat(result.getExitCode(), is(Cmd.ExitCode.SHELL_DIED)); 35 | assertThat(result.getCmd(), is(cmd)); 36 | assertThat(result.getOutput(), is(not(nullValue()))); 37 | assertThat(result.getErrors(), is(not(nullValue()))); 38 | } 39 | 40 | @Test 41 | public void testConstructor3() { 42 | List output = new ArrayList<>(); 43 | List errors = new ArrayList<>(); 44 | Cmd.Result result = new Cmd.Result(cmd, Cmd.ExitCode.SHELL_DIED, output, errors); 45 | assertThat(result.getExitCode(), is(Cmd.ExitCode.SHELL_DIED)); 46 | assertThat(result.getCmd(), is(cmd)); 47 | assertThat(result.getOutput(), is(output)); 48 | assertThat(result.getErrors(), is(errors)); 49 | } 50 | 51 | @Test 52 | public void testMerge() { 53 | Cmd.Result emptyResult = new Cmd.Result(cmd); 54 | assertThat(emptyResult.merge(), is(not(nullValue()))); 55 | 56 | List output = Collections.singletonList("output"); 57 | List errors = Collections.singletonList("errors"); 58 | Cmd.Result result = new Cmd.Result(cmd, Cmd.ExitCode.SHELL_DIED, output, errors); 59 | assertThat(result.merge(), contains("output", "errors")); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/cmd/HarvesterTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.cmd; 2 | 3 | import org.hamcrest.Matchers; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.MockitoJUnitRunner; 8 | 9 | import java.util.UUID; 10 | import java.util.concurrent.CountDownLatch; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import io.reactivex.rxjava3.processors.PublishProcessor; 14 | import io.reactivex.rxjava3.processors.ReplayProcessor; 15 | import io.reactivex.rxjava3.subscribers.TestSubscriber; 16 | import testtools.BaseTest; 17 | 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.contains; 20 | import static org.hamcrest.Matchers.nullValue; 21 | import static org.hamcrest.core.Is.is; 22 | import static org.mockito.Mockito.when; 23 | 24 | @RunWith(MockitoJUnitRunner.class) 25 | public class HarvesterTest extends BaseTest { 26 | @Mock Cmd cmd; 27 | PublishProcessor publisher; 28 | Harvester.Factory harvesterFactory; 29 | 30 | @Override 31 | public void setup() throws Exception { 32 | super.setup(); 33 | harvesterFactory = new Harvester.Factory(); 34 | publisher = PublishProcessor.create(); 35 | } 36 | 37 | @Test 38 | public void testCommandCompletion_output() { 39 | String uuid = UUID.randomUUID().toString(); 40 | when(cmd.getMarker()).thenReturn(uuid); 41 | 42 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forOutput(publisher, cmd)).test(); 43 | testSubscriber.assertNoErrors(); 44 | 45 | publisher.onNext(uuid + " 255"); 46 | 47 | testSubscriber.assertValueCount(1).assertComplete(); 48 | 49 | OutputHarvester.Crop crop = testSubscriber.values().get(0); 50 | assertThat(crop.exitCode, is(255)); 51 | } 52 | 53 | @Test 54 | public void testCommandCompletion_errors() { 55 | String uuid = UUID.randomUUID().toString(); 56 | when(cmd.getMarker()).thenReturn(uuid); 57 | 58 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forError(publisher, cmd)).test(); 59 | testSubscriber.assertNoErrors(); 60 | 61 | publisher.onNext(uuid); 62 | 63 | testSubscriber.assertValueCount(1).assertComplete(); 64 | } 65 | 66 | @Test 67 | public void testBuffers_output() { 68 | String uuid = UUID.randomUUID().toString(); 69 | when(cmd.getMarker()).thenReturn(uuid); 70 | when(cmd.isOutputBufferEnabled()).thenReturn(true); 71 | 72 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forOutput(publisher, cmd)).test(); 73 | testSubscriber.assertNoErrors(); 74 | 75 | publisher.onNext("some-output"); 76 | publisher.onNext(uuid + " 255"); 77 | 78 | testSubscriber.assertValueCount(1).assertComplete(); 79 | 80 | Harvester.Crop crop = testSubscriber.values().get(0); 81 | assertThat(crop.buffer.size(), is(1)); 82 | assertThat(crop.buffer, Matchers.contains("some-output")); 83 | } 84 | 85 | @Test 86 | public void testBuffers_error() { 87 | String uuid = UUID.randomUUID().toString(); 88 | when(cmd.getMarker()).thenReturn(uuid); 89 | when(cmd.isErrorBufferEnabled()).thenReturn(true); 90 | 91 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forError(publisher, cmd)).test(); 92 | testSubscriber.assertNoErrors(); 93 | 94 | publisher.onNext("some-errors"); 95 | publisher.onNext(uuid + " 255"); 96 | 97 | testSubscriber.assertValueCount(1).assertComplete(); 98 | 99 | Harvester.Crop crop = testSubscriber.values().get(0); 100 | assertThat(crop.buffer.size(), is(1)); 101 | assertThat(crop.buffer, Matchers.contains("some-errors")); 102 | } 103 | 104 | @Test 105 | public void testUpstreamPrematureCompletion_output() { 106 | String uuid = UUID.randomUUID().toString(); 107 | when(cmd.getMarker()).thenReturn(uuid); 108 | when(cmd.isOutputBufferEnabled()).thenReturn(true); 109 | 110 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forOutput(publisher, cmd)).test(); 111 | testSubscriber.assertNoErrors(); 112 | 113 | publisher.onNext("some-output"); 114 | publisher.onComplete(); 115 | 116 | OutputHarvester.Crop crop = testSubscriber.assertValueCount(1).assertComplete().values().get(0); 117 | assertThat(crop.isComplete, is(false)); 118 | assertThat(crop.exitCode, is(Cmd.ExitCode.INITIAL)); 119 | assertThat(crop.buffer.size(), is(1)); 120 | assertThat(crop.buffer, contains("some-output")); 121 | } 122 | 123 | @Test 124 | public void testUpstreamPrematureCompletion_errors() { 125 | String uuid = UUID.randomUUID().toString(); 126 | when(cmd.getMarker()).thenReturn(uuid); 127 | when(cmd.isErrorBufferEnabled()).thenReturn(true); 128 | 129 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forError(publisher, cmd)).test(); 130 | testSubscriber.assertNoErrors(); 131 | 132 | publisher.onNext("some-errors"); 133 | publisher.onComplete(); 134 | 135 | ErrorHarvester.Crop crop = testSubscriber.assertValueCount(1).assertComplete().values().get(0); 136 | assertThat(crop.isComplete, is(false)); 137 | assertThat(crop.buffer.size(), is(1)); 138 | assertThat(crop.buffer, contains("some-errors")); 139 | } 140 | 141 | @Test 142 | public void testUpstreamTerminated_output() { 143 | publisher.onComplete(); 144 | OutputHarvester.Crop crop = publisher.compose(harvesterFactory.forOutput(publisher, cmd)).test().assertComplete().assertValueCount(1).values().get(0); 145 | assertThat(crop.isComplete, is(false)); 146 | 147 | publisher = PublishProcessor.create(); 148 | publisher.onError(new InterruptedException()); 149 | crop = publisher.compose(harvesterFactory.forOutput(publisher, cmd)).test().assertComplete().assertValueCount(1).values().get(0); 150 | assertThat(crop.isComplete, is(false)); 151 | } 152 | 153 | @Test 154 | public void testUpstreamTerminated_error() { 155 | publisher.onComplete(); 156 | ErrorHarvester.Crop crop = publisher.compose(harvesterFactory.forError(publisher, cmd)).test().assertComplete().assertValueCount(1).values().get(0); 157 | assertThat(crop.isComplete, is(false)); 158 | 159 | publisher = PublishProcessor.create(); 160 | publisher.onError(new InterruptedException()); 161 | crop = publisher.compose(harvesterFactory.forError(publisher, cmd)).test().assertComplete().assertValueCount(1).values().get(0); 162 | assertThat(crop.isComplete, is(false)); 163 | } 164 | 165 | @Test 166 | public void testDownstreamCancel_output() throws InterruptedException { 167 | CountDownLatch latch = new CountDownLatch(1); 168 | TestSubscriber testSubscriber = publisher.doOnCancel(latch::countDown).compose(harvesterFactory.forOutput(publisher, cmd)).test(); 169 | testSubscriber.assertNoErrors(); 170 | 171 | testSubscriber.cancel(); 172 | 173 | assertThat(latch.await(1, TimeUnit.SECONDS), is(true)); 174 | } 175 | 176 | @Test 177 | public void testDownstreamCancel_errors() throws InterruptedException { 178 | CountDownLatch latch = new CountDownLatch(1); 179 | TestSubscriber testSubscriber = publisher.doOnCancel(latch::countDown).compose(harvesterFactory.forOutput(publisher, cmd)).test(); 180 | testSubscriber.assertNoErrors(); 181 | 182 | testSubscriber.cancel(); 183 | 184 | assertThat(latch.await(1, TimeUnit.SECONDS), is(true)); 185 | } 186 | 187 | @Test 188 | public void testProcessors_output() { 189 | String uuid = UUID.randomUUID().toString(); 190 | when(cmd.getMarker()).thenReturn(uuid); 191 | ReplayProcessor processor = ReplayProcessor.create(); 192 | when(cmd.getOutputProcessor()).thenReturn(processor); 193 | 194 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forOutput(publisher, cmd)).test(); 195 | 196 | publisher.onNext("some-output"); 197 | publisher.onNext(uuid + " 255"); 198 | 199 | processor.test().awaitDone(1, TimeUnit.SECONDS).assertNoErrors().assertValueCount(1).assertValue("some-output"); 200 | OutputHarvester.Crop crop = testSubscriber.awaitDone(1, TimeUnit.SECONDS).assertNoErrors().assertValueCount(1).values().get(0); 201 | assertThat(crop.exitCode, is(255)); 202 | assertThat(crop.buffer, is(nullValue())); 203 | } 204 | 205 | @Test 206 | public void testProcessors_errors() { 207 | String uuid = UUID.randomUUID().toString(); 208 | when(cmd.getMarker()).thenReturn(uuid); 209 | ReplayProcessor processor = ReplayProcessor.create(); 210 | when(cmd.getErrorProcessor()).thenReturn(processor); 211 | 212 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forError(publisher, cmd)).test(); 213 | 214 | publisher.onNext("some-errors"); 215 | publisher.onNext(uuid); 216 | 217 | processor.test().awaitDone(1, TimeUnit.SECONDS).assertNoErrors().assertValueCount(1).assertValue("some-errors"); 218 | Harvester.Crop crop = testSubscriber.awaitDone(1, TimeUnit.SECONDS).assertNoErrors().assertValueCount(1).values().get(0); 219 | assertThat(crop.buffer, is(nullValue())); 220 | } 221 | 222 | @Test 223 | public void testBadMarker_output() { 224 | String uuid = UUID.randomUUID().toString(); 225 | when(cmd.getMarker()).thenReturn(uuid); 226 | 227 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forOutput(publisher, cmd)).test(); 228 | testSubscriber.assertNoErrors(); 229 | 230 | publisher.onNext(uuid + " &/()"); 231 | 232 | testSubscriber.awaitDone(1, TimeUnit.SECONDS).assertNoErrors(); 233 | OutputHarvester.Crop crop = testSubscriber.values().get(0); 234 | assertThat(crop.exitCode, is(Cmd.ExitCode.EXCEPTION)); 235 | } 236 | 237 | @Test 238 | public void testBadMarker_errors() { 239 | String uuid = UUID.randomUUID().toString(); 240 | when(cmd.getMarker()).thenReturn(uuid); 241 | 242 | TestSubscriber testSubscriber = publisher.compose(harvesterFactory.forError(publisher, cmd)).test(); 243 | testSubscriber.assertNoErrors(); 244 | 245 | publisher.onNext(uuid + " §$%&"); 246 | 247 | testSubscriber.awaitDone(1, TimeUnit.SECONDS).assertNoErrors().assertValueCount(1); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/cmd/RxCmdShellBuilderTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.cmd; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.junit.MockitoJUnitRunner; 6 | 7 | import java.util.Collections; 8 | 9 | import eu.darken.rxshell.extra.EnvVar; 10 | import eu.darken.rxshell.extra.HasEnvironmentVariables; 11 | import testtools.BaseTest; 12 | 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.Matchers.not; 15 | import static org.hamcrest.Matchers.nullValue; 16 | import static org.hamcrest.core.Is.is; 17 | import static org.mockito.Mockito.mock; 18 | import static org.mockito.Mockito.verify; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class RxCmdShellBuilderTest extends BaseTest { 22 | 23 | @Test 24 | public void testInstantiation() { 25 | final RxCmdShell shell = RxCmdShell.builder().build(); 26 | assertThat(shell, is(not(nullValue()))); 27 | } 28 | 29 | @Test 30 | public void testEnvironmentBuilding() { 31 | RxCmdShell.Builder shellBuilder = RxCmdShell.builder(); 32 | assertThat(shellBuilder.getEnvironment().isEmpty(), is(true)); 33 | 34 | final EnvVar testEnvVar = new EnvVar<>("1", "2"); 35 | shellBuilder.shellEnvironment(root -> { 36 | assertThat(root, is(false)); 37 | return Collections.singletonList(testEnvVar); 38 | }); 39 | shellBuilder.build(); 40 | assertThat(shellBuilder.getEnvironment().size(), is(1)); 41 | } 42 | 43 | @Test 44 | public void testEnvironmentBuilding_useRoot() { 45 | RxCmdShell.Builder shellBuilder = RxCmdShell.builder(); 46 | HasEnvironmentVariables m = mock(HasEnvironmentVariables.class); 47 | shellBuilder.shellEnvironment(m); 48 | shellBuilder.build(); 49 | verify(m).getEnvironmentVariables(false); 50 | shellBuilder.root(true); 51 | shellBuilder.build(); 52 | verify(m).getEnvironmentVariables(true); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/cmd/RxCmdShellHelperTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.cmd; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.junit.MockitoJUnitRunner; 6 | 7 | import java.io.IOException; 8 | 9 | import eu.darken.rxshell.extra.RxCmdShellHelper; 10 | import testtools.BaseTest; 11 | 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.when; 14 | 15 | @RunWith(MockitoJUnitRunner.class) 16 | public class RxCmdShellHelperTest extends BaseTest { 17 | 18 | @Test(expected = RuntimeException.class) 19 | public void blockingOpenBuilder_unchecked() throws IOException { 20 | RxCmdShell.Builder builder = mock(RxCmdShell.Builder.class); 21 | RxCmdShell shell = mock(RxCmdShell.class); 22 | when(builder.build()).thenReturn(shell); 23 | when(shell.open()).thenThrow(new RuntimeException(new Exception())); 24 | RxCmdShellHelper.blockingOpen(builder); 25 | } 26 | 27 | @Test(expected = IOException.class) 28 | public void blockingOpenBuilder_checked() throws IOException { 29 | RxCmdShell.Builder builder = mock(RxCmdShell.Builder.class); 30 | RxCmdShell shell = mock(RxCmdShell.class); 31 | when(builder.build()).thenReturn(shell); 32 | when(shell.open()).thenThrow(new RuntimeException(new IOException())); 33 | RxCmdShellHelper.blockingOpen(builder); 34 | } 35 | 36 | 37 | @Test(expected = RuntimeException.class) 38 | public void blockingOpenShell_unchecked() throws IOException { 39 | RxCmdShell shell = mock(RxCmdShell.class); 40 | when(shell.open()).thenThrow(new RuntimeException(new Exception())); 41 | RxCmdShellHelper.blockingOpen(shell); 42 | } 43 | 44 | @Test(expected = IOException.class) 45 | public void blockingOpenShell_checked() throws IOException { 46 | RxCmdShell shell = mock(RxCmdShell.class); 47 | when(shell.open()).thenThrow(new RuntimeException(new IOException())); 48 | RxCmdShellHelper.blockingOpen(shell); 49 | } 50 | 51 | @Test(expected = RuntimeException.class) 52 | public void blockinCancel_unchecked() throws IOException { 53 | RxCmdShell.Session session = mock(RxCmdShell.Session.class); 54 | when(session.cancel()).thenThrow(new RuntimeException(new Exception())); 55 | RxCmdShellHelper.blockingCancel(session); 56 | } 57 | 58 | @Test(expected = IOException.class) 59 | public void blockingCancel_checked() throws IOException { 60 | RxCmdShell.Session session = mock(RxCmdShell.Session.class); 61 | when(session.cancel()).thenThrow(new RuntimeException(new IOException())); 62 | RxCmdShellHelper.blockingCancel(session); 63 | } 64 | 65 | @Test 66 | public void blockingCancel_nullable() throws IOException { 67 | RxCmdShellHelper.blockingCancel(null); 68 | } 69 | 70 | @Test(expected = RuntimeException.class) 71 | public void blockinClose_unchecked() throws IOException { 72 | RxCmdShell.Session session = mock(RxCmdShell.Session.class); 73 | when(session.close()).thenThrow(new RuntimeException(new Exception())); 74 | RxCmdShellHelper.blockingClose(session); 75 | } 76 | 77 | @Test(expected = IOException.class) 78 | public void blockingClose_checked() throws IOException { 79 | RxCmdShell.Session session = mock(RxCmdShell.Session.class); 80 | when(session.close()).thenThrow(new RuntimeException(new IOException())); 81 | RxCmdShellHelper.blockingClose(session); 82 | } 83 | 84 | @Test 85 | public void blockingClose_nullable() throws IOException { 86 | RxCmdShellHelper.blockingClose(null); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/extra/ApiWrapTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.extra; 2 | 3 | import android.os.Build; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.junit.MockitoJUnitRunner; 8 | 9 | import testtools.BaseTest; 10 | 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | import static org.hamcrest.core.Is.is; 13 | 14 | @RunWith(MockitoJUnitRunner.class) 15 | public class ApiWrapTest extends BaseTest { 16 | 17 | @Test 18 | public void testDefault() { 19 | ApiWrap.setSDKInt(55); 20 | assertThat(ApiWrap.getCurrentSDKInt(), is(55)); 21 | } 22 | 23 | @Test 24 | public void testJellyBeanMR2() { 25 | ApiWrap.setSDKInt(0); 26 | assertThat(ApiWrap.hasJellyBeanMR2(), is(false)); 27 | ApiWrap.setSDKInt(Build.VERSION_CODES.JELLY_BEAN_MR2); 28 | assertThat(ApiWrap.hasJellyBeanMR2(), is(true)); 29 | ApiWrap.setSDKInt(Build.VERSION_CODES.KITKAT); 30 | assertThat(ApiWrap.hasJellyBeanMR2(), is(true)); 31 | } 32 | 33 | @Test 34 | public void testKitKat() { 35 | ApiWrap.setSDKInt(0); 36 | assertThat(ApiWrap.hasKitKat(), is(false)); 37 | ApiWrap.setSDKInt(Build.VERSION_CODES.KITKAT); 38 | assertThat(ApiWrap.hasKitKat(), is(true)); 39 | ApiWrap.setSDKInt(Build.VERSION_CODES.KITKAT_WATCH); 40 | assertThat(ApiWrap.hasKitKat(), is(true)); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/extra/RXSDebugTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.extra; 2 | 3 | 4 | import org.hamcrest.MatcherAssert; 5 | import org.hamcrest.core.Is; 6 | import org.junit.Test; 7 | 8 | import eu.darken.rxshell.BuildConfig; 9 | 10 | import static org.hamcrest.MatcherAssert.assertThat; 11 | import static org.hamcrest.core.Is.is; 12 | 13 | public class RXSDebugTest { 14 | 15 | @Test 16 | public void test() { 17 | MatcherAssert.assertThat(RXSDebug.isDebug(), Is.is(BuildConfig.DEBUG)); 18 | RXSDebug.setDebug(false); 19 | assertThat(RXSDebug.isDebug(), is(false)); 20 | RXSDebug.setDebug(true); 21 | assertThat(RXSDebug.isDebug(), is(true)); 22 | } 23 | 24 | @Test 25 | public void testProcessCallbacks() { 26 | for (RXSDebug.Callback callback : RXSDebug.CALLBACKS) { 27 | RXSDebug.removeCallback(callback); 28 | } 29 | 30 | RXSDebug.ProcessCallback callback = new RXSDebug.ProcessCallback() { 31 | @Override 32 | public void onProcessStart(Process process) { 33 | 34 | } 35 | 36 | @Override 37 | public void onProcessEnd(Process process) { 38 | 39 | } 40 | }; 41 | assertThat(RXSDebug.CALLBACKS.isEmpty(), is(true)); 42 | RXSDebug.addCallback(callback); 43 | RXSDebug.addCallback(callback); 44 | assertThat(RXSDebug.CALLBACKS.size(), is(1)); 45 | RXSDebug.removeCallback(callback); 46 | assertThat(RXSDebug.CALLBACKS.size(), is(0)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/process/DefaultProcessFactoryTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.IOException; 6 | 7 | import testtools.BaseTest; 8 | 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.Matchers.not; 11 | import static org.hamcrest.Matchers.nullValue; 12 | import static org.hamcrest.core.Is.is; 13 | 14 | public class DefaultProcessFactoryTest extends BaseTest { 15 | 16 | @Test 17 | public void testStart() throws IOException { 18 | DefaultProcessFactory pf = new DefaultProcessFactory(); 19 | Process process = pf.start("id"); 20 | assertThat(process, is(not(nullValue()))); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/process/ProcessHelperTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.MockitoJUnitRunner; 8 | 9 | import eu.darken.rxshell.extra.ApiWrap; 10 | import testtools.BaseTest; 11 | 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.core.Is.is; 14 | import static org.mockito.Mockito.doReturn; 15 | import static org.mockito.Mockito.doThrow; 16 | import static org.mockito.Mockito.when; 17 | 18 | @RunWith(MockitoJUnitRunner.class) 19 | public class ProcessHelperTest extends BaseTest { 20 | @Mock Process process; 21 | 22 | @Test 23 | public void testIsAlive_legacy() { 24 | ApiWrap.setSDKInt(16); 25 | doThrow(new IllegalThreadStateException()).when(process).exitValue(); 26 | assertThat(ProcessHelper.isAlive(process), is(true)); 27 | 28 | doReturn(0).when(process).exitValue(); 29 | assertThat(ProcessHelper.isAlive(process), is(false)); 30 | } 31 | 32 | @Test 33 | public void testIsAlive_oreo() { 34 | ApiWrap.setSDKInt(26); 35 | when(process.isAlive()).thenReturn(true); 36 | assertThat(ProcessHelper.isAlive(process), is(true)); 37 | 38 | when(process.isAlive()).thenReturn(false); 39 | assertThat(ProcessHelper.isAlive(process), is(false)); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/process/RootKillerTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.MockitoJUnitRunner; 9 | 10 | import java.io.IOException; 11 | import java.util.concurrent.LinkedBlockingQueue; 12 | 13 | import eu.darken.rxshell.extra.ApiWrap; 14 | import eu.darken.rxshell.shell.LineReader; 15 | import testtools.BaseTest; 16 | import testtools.MockProcess; 17 | 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.notNullValue; 20 | import static org.hamcrest.Matchers.nullValue; 21 | import static org.hamcrest.core.Is.is; 22 | import static org.mockito.ArgumentMatchers.any; 23 | import static org.mockito.Mockito.never; 24 | import static org.mockito.Mockito.verify; 25 | import static org.mockito.Mockito.when; 26 | 27 | @RunWith(MockitoJUnitRunner.class) 28 | public class RootKillerTest extends BaseTest { 29 | @Mock ProcessFactory processFactory; 30 | @Mock Process processtoKill; 31 | 32 | MockProcess psProcess; 33 | MockProcess killProcess; 34 | LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); 35 | 36 | @Before 37 | public void setup() throws Exception { 38 | super.setup(); 39 | psProcess = new MockProcess(); 40 | killProcess = new MockProcess(); 41 | queue.add(psProcess); 42 | queue.add(killProcess); 43 | when(processFactory.start(any())).thenAnswer(invocation -> queue.take()); 44 | } 45 | 46 | @Test 47 | public void testFactory() { 48 | ProcessKiller killer = new RootKiller(processFactory); 49 | assertThat(killer, is(notNullValue())); 50 | } 51 | 52 | @Test 53 | public void testKill() { 54 | ApiWrap.setSDKInt(26); 55 | when(processtoKill.isAlive()).thenReturn(true); 56 | when(processtoKill.toString()).thenReturn("Process[pid=1234, hasExited=false]"); 57 | 58 | ProcessKiller killer = new RootKiller(processFactory); 59 | 60 | psProcess.addCmdListener(line -> { 61 | if (line.startsWith("ps")) { 62 | psProcess.printData(" PID PPID PGID WINPID TTY UID STIME COMMAND"); 63 | psProcess.printData(" 5678 1234 6316 7860 pty1 1001 Nov 4 /usr/bin/ssh"); 64 | } 65 | return true; 66 | }); 67 | 68 | assertThat(ProcessHelper.isAlive(processtoKill), is(true)); 69 | assertThat(killer.kill(processtoKill), is(true)); 70 | assertThat(killProcess.getLastCommandRaw(), is("kill 1234" + LineReader.getLineSeparator())); 71 | } 72 | 73 | @Test 74 | public void testKill_dontKillTheDead() throws IOException { 75 | ApiWrap.setSDKInt(26); 76 | when(processtoKill.isAlive()).thenReturn(false); 77 | 78 | ProcessKiller killer = new RootKiller(processFactory); 79 | 80 | assertThat(killer.kill(processtoKill), is(true)); 81 | 82 | verify(processFactory, never()).start(any()); 83 | } 84 | 85 | @Test 86 | public void testKill_cantFindProcessPid() throws IOException { 87 | ApiWrap.setSDKInt(26); 88 | when(processtoKill.isAlive()).thenReturn(true); 89 | 90 | ProcessKiller killer = new RootKiller(processFactory); 91 | 92 | when(processtoKill.toString()).thenReturn(""); 93 | assertThat(psProcess.getLastCommandRaw(), is(nullValue())); 94 | assertThat(killer.kill(processtoKill), is(false)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/process/UserKillerTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.process; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.MockitoJUnitRunner; 7 | 8 | import eu.darken.rxshell.extra.ApiWrap; 9 | import testtools.BaseTest; 10 | 11 | import static org.mockito.Mockito.verify; 12 | import static org.mockito.Mockito.when; 13 | 14 | @RunWith(MockitoJUnitRunner.class) 15 | public class UserKillerTest extends BaseTest { 16 | @Mock Process process; 17 | 18 | @Test 19 | public void testKill_legacy() { 20 | ApiWrap.setSDKInt(16); 21 | when(process.exitValue()).thenThrow(new IllegalThreadStateException()); 22 | final ProcessKiller killer = new UserKiller(); 23 | killer.kill(process); 24 | verify(process).destroy(); 25 | } 26 | 27 | @Test 28 | public void testKill_oreo() { 29 | ApiWrap.setSDKInt(26); 30 | when(process.isAlive()).thenReturn(true); 31 | final ProcessKiller killer = new UserKiller(); 32 | killer.kill(process); 33 | verify(process).destroyForcibly(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/test/java/eu/darken/rxshell/shell/LineReaderTest.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshell.shell; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.junit.MockitoJUnitRunner; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.IOException; 9 | import java.io.InputStreamReader; 10 | import java.io.Reader; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import eu.darken.rxshell.extra.ApiWrap; 15 | import testtools.BaseTest; 16 | 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.hamcrest.Matchers.notNullValue; 19 | import static org.hamcrest.core.Is.is; 20 | import static testtools.StreamHelper.makeStream; 21 | 22 | @RunWith(MockitoJUnitRunner.class) 23 | public class LineReaderTest extends BaseTest { 24 | 25 | @Test 26 | public void testGetLineSeperator() { 27 | assertThat(LineReader.getLineSeparator(), is(notNullValue())); 28 | ApiWrap.setSDKInt(26); 29 | assertThat(LineReader.getLineSeparator(), is(System.lineSeparator())); 30 | ApiWrap.setSDKInt(16); 31 | assertThat(LineReader.getLineSeparator(), is(System.getProperty("line.separator", "\n"))); 32 | } 33 | 34 | @Test 35 | public void testLineEndings_linux() throws IOException { 36 | final List output = new ArrayList<>(); 37 | final LineReader reader = new LineReader("\n"); 38 | Reader stream = new BufferedReader(new InputStreamReader(makeStream("line1\r\nline2\r\nli\rne\n\n"))); 39 | String line; 40 | while ((line = reader.readLine(stream)) != null) { 41 | output.add(line); 42 | } 43 | assertThat(output.size(), is(4)); 44 | assertThat(output.get(0), is("line1\r")); 45 | assertThat(output.get(1), is("line2\r")); 46 | assertThat(output.get(2), is("li\rne")); 47 | assertThat(output.get(3), is("")); 48 | } 49 | 50 | @Test 51 | public void testLineEndings_windows() throws IOException { 52 | final List output = new ArrayList<>(); 53 | final LineReader reader = new LineReader("\r\n"); 54 | Reader stream = new BufferedReader(new InputStreamReader(makeStream("line1\r\nline2\r\n\r\n"))); 55 | String line; 56 | while ((line = reader.readLine(stream)) != null) { 57 | output.add(line); 58 | } 59 | assertThat(output.size(), is(3)); 60 | assertThat(output.get(0), is("line1")); 61 | assertThat(output.get(1), is("line2")); 62 | assertThat(output.get(2), is("")); 63 | } 64 | 65 | @Test 66 | public void testLineEndings_legacy() throws IOException { 67 | final List output = new ArrayList<>(); 68 | final LineReader reader = new LineReader("\r"); 69 | Reader stream = new BufferedReader(new InputStreamReader(makeStream("line1\n\rline2\n\rli\nne\r\r"))); 70 | String line; 71 | while ((line = reader.readLine(stream)) != null) { 72 | output.add(line); 73 | } 74 | assertThat(output.size(), is(4)); 75 | assertThat(output.get(0), is("line1\n")); 76 | assertThat(output.get(1), is("line2\n")); 77 | assertThat(output.get(2), is("li\nne")); 78 | assertThat(output.get(3), is("")); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /core/src/test/java/testtools/BaseTest.java: -------------------------------------------------------------------------------- 1 | package testtools; 2 | 3 | 4 | import org.junit.After; 5 | import org.junit.Before; 6 | 7 | import java.util.List; 8 | 9 | import timber.log.Timber; 10 | 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | import static org.hamcrest.core.Is.is; 13 | 14 | public class BaseTest { 15 | List uncaughtExceptions; 16 | 17 | @Before 18 | public void setup() throws Exception { 19 | uncaughtExceptions = TestHelper.trackPluginErrors(); 20 | Timber.plant(new JUnitTree()); 21 | } 22 | 23 | @After 24 | public void tearDown() { 25 | TestHelper.sleep(100); 26 | for (Throwable t : uncaughtExceptions) t.printStackTrace(); 27 | assertThat(uncaughtExceptions.toString(), uncaughtExceptions.isEmpty(), is(true)); 28 | Timber.uprootAll(); 29 | } 30 | 31 | public List getUncaughtExceptions() { 32 | return uncaughtExceptions; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/test/java/testtools/JUnitTree.java: -------------------------------------------------------------------------------- 1 | package testtools; 2 | 3 | 4 | import android.support.annotation.NonNull; 5 | import android.util.Log; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import timber.log.Timber; 11 | 12 | public class JUnitTree extends Timber.DebugTree { 13 | private final int minlogLevel; 14 | private final List lines = new ArrayList<>(); 15 | 16 | public JUnitTree() { 17 | minlogLevel = Log.VERBOSE; 18 | } 19 | 20 | public JUnitTree(int minlogLevel) { 21 | this.minlogLevel = minlogLevel; 22 | } 23 | 24 | private static String priorityToString(int priority) { 25 | switch (priority) { 26 | case Log.ERROR: 27 | return "E"; 28 | case Log.WARN: 29 | return "W"; 30 | case Log.INFO: 31 | return "I"; 32 | case Log.DEBUG: 33 | return "D"; 34 | case Log.VERBOSE: 35 | return "V"; 36 | default: 37 | return String.valueOf(priority); 38 | } 39 | } 40 | 41 | @Override 42 | protected void log(int priority, String tag, @NonNull String message, Throwable t) { 43 | if (priority < minlogLevel) return; 44 | final String line = System.currentTimeMillis() + " " + priorityToString(priority) + "/" + tag + ": " + message; 45 | lines.add(line); 46 | System.out.println(line); 47 | } 48 | 49 | public List getLines() { 50 | return lines; 51 | } 52 | } -------------------------------------------------------------------------------- /core/src/test/java/testtools/MockInputStream.java: -------------------------------------------------------------------------------- 1 | package testtools; 2 | 3 | 4 | import android.support.annotation.NonNull; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.PipedInputStream; 9 | import java.io.PipedOutputStream; 10 | 11 | import timber.log.Timber; 12 | 13 | public class MockInputStream extends InputStream { 14 | private final PipedInputStream reader; 15 | private final PipedOutputStream writer; 16 | private boolean isOpen = true; 17 | 18 | public MockInputStream() throws IOException { 19 | writer = new PipedOutputStream(); 20 | reader = new PipedInputStream(writer); 21 | } 22 | 23 | public void queue(String data) { 24 | try { 25 | writer.write(data.getBytes()); 26 | writer.flush(); 27 | Timber.v("Written&Flushed: %s", data); 28 | } catch (IOException e) { 29 | e.printStackTrace(); 30 | } 31 | } 32 | 33 | @Override 34 | public int read() throws IOException { 35 | return reader.read(); 36 | } 37 | 38 | @Override 39 | public int read(@NonNull byte[] b) throws IOException { 40 | return reader.read(b); 41 | } 42 | 43 | @Override 44 | public int read(@NonNull byte[] b, int off, int len) throws IOException { 45 | return reader.read(b, off, len); 46 | } 47 | 48 | @Override 49 | public long skip(long n) throws IOException { 50 | return reader.skip(n); 51 | } 52 | 53 | @Override 54 | public int available() throws IOException { 55 | return reader.available(); 56 | } 57 | 58 | @Override 59 | public void mark(int readlimit) { 60 | reader.mark(readlimit); 61 | } 62 | 63 | @Override 64 | public void reset() throws IOException { 65 | reader.reset(); 66 | } 67 | 68 | @Override 69 | public boolean markSupported() { 70 | return reader.markSupported(); 71 | } 72 | 73 | public boolean isOpen() { 74 | return isOpen; 75 | } 76 | 77 | @Override 78 | public void close() throws IOException { 79 | Timber.d("close() called"); 80 | if (!isOpen) return; 81 | isOpen = false; 82 | reader.close(); 83 | writer.flush(); 84 | writer.close(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /core/src/test/java/testtools/MockOutputStream.java: -------------------------------------------------------------------------------- 1 | package testtools; 2 | 3 | 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | 7 | import timber.log.Timber; 8 | 9 | public class MockOutputStream extends OutputStream { 10 | private final Listener listener; 11 | private final Object dataSync = new Object(); 12 | private StringBuilder data = new StringBuilder(); 13 | private boolean isOpen = true; 14 | private StringBuffer stringBuffer = new StringBuffer(); 15 | private IOException closeException; 16 | 17 | public MockOutputStream(Listener listener) {this.listener = listener;} 18 | 19 | @Override 20 | public void write(int i) throws IOException { 21 | synchronized (dataSync) { 22 | Character c = (char) i; 23 | data.append(c); 24 | stringBuffer.append((char) i); 25 | if (c == '\n') { 26 | final String line = data.toString(); 27 | Timber.d("Line: %s", line); 28 | if (listener != null) listener.onNewLine(line); 29 | data = new StringBuilder(); 30 | } 31 | } 32 | } 33 | 34 | @Override 35 | public synchronized void close() throws IOException { 36 | Timber.d("close() called"); 37 | if (closeException != null) { 38 | isOpen = false; 39 | IOException e = closeException; 40 | closeException = null; 41 | throw e; 42 | } 43 | 44 | if (!isOpen) return; 45 | isOpen = false; 46 | 47 | if (listener != null) listener.onClose(); 48 | } 49 | 50 | public void setExceptionOnClose(IOException closeException) { 51 | this.closeException = closeException; 52 | } 53 | 54 | public boolean isOpen() { 55 | return isOpen; 56 | } 57 | 58 | public StringBuffer getData() { 59 | return stringBuffer; 60 | } 61 | 62 | public interface Listener { 63 | void onNewLine(String line); 64 | 65 | void onClose(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/src/test/java/testtools/MockProcess.java: -------------------------------------------------------------------------------- 1 | package testtools; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.concurrent.CountDownLatch; 11 | import java.util.concurrent.LinkedBlockingQueue; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import eu.darken.rxshell.process.RxProcess; 15 | import eu.darken.rxshell.shell.LineReader; 16 | import timber.log.Timber; 17 | 18 | 19 | public class MockProcess extends Process { 20 | final List cmdListeners = new ArrayList<>(); 21 | final MockInputStream dataStream; 22 | final MockInputStream errorStream; 23 | final MockOutputStream cmdStream; 24 | final LinkedBlockingQueue cmdLines = new LinkedBlockingQueue<>(); 25 | final LinkedBlockingQueue processorQueue = new LinkedBlockingQueue<>(); 26 | volatile Integer exitCode = null; 27 | volatile boolean isDestroyed = false; 28 | final CountDownLatch exitLatch = new CountDownLatch(1); 29 | final Thread processor; 30 | 31 | public MockProcess() throws IOException { 32 | dataStream = new MockInputStream(); 33 | errorStream = new MockInputStream(); 34 | cmdStream = new MockOutputStream(new MockOutputStream.Listener() { 35 | @Override 36 | public void onNewLine(String line) { 37 | cmdLines.add(line); 38 | processorQueue.add(line); 39 | } 40 | 41 | @Override 42 | public void onClose() { 43 | Timber.v("CmdStream: onClose()"); 44 | } 45 | }); 46 | 47 | processor = new Thread(() -> { 48 | // We keep processing while there is more input or could be more input 49 | while (cmdStream.isOpen() || processorQueue.size() > 0) { 50 | // Timber.v("CommandProcessor: isInputOpen=%b, queued='%s", cmdStream.isOpen(), processorQueue); 51 | String line = null; 52 | try { 53 | line = processorQueue.poll(100, TimeUnit.MILLISECONDS); 54 | } catch (InterruptedException e) { Timber.e(e); } 55 | if (line == null) continue; 56 | 57 | boolean alreadyProcessed = false; 58 | for (CmdListener l : cmdListeners) { 59 | if (l.onNewCmd(line)) alreadyProcessed = true; 60 | } 61 | 62 | if (!alreadyProcessed) { 63 | if (line.endsWith(" $?" + LineReader.getLineSeparator())) { 64 | 65 | // By default we assume all commands exit OK 66 | final String[] split = line.split(" "); 67 | printData(split[1] + " " + 0); 68 | 69 | } else if (line.endsWith(" >&2" + LineReader.getLineSeparator())) { 70 | 71 | final String[] split = line.split(" "); 72 | printError(split[1]); 73 | 74 | } else if (line.startsWith("sleep")) { 75 | 76 | final String[] split = line.replace(LineReader.getLineSeparator(), "").split(" "); 77 | long delay = Long.parseLong(split[1]); 78 | Timber.v("Sleeping for %d", delay); 79 | try { Thread.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } 80 | 81 | } else if (line.startsWith("echo")) { 82 | 83 | final String[] split = line.replace(LineReader.getLineSeparator(), "").split(" "); 84 | printData(split[1]); 85 | 86 | } else if (line.startsWith("error")) { 87 | 88 | final String[] split = line.replace(LineReader.getLineSeparator(), "").split(" "); 89 | printError(split[1]); 90 | 91 | } else if (line.startsWith("exit")) { 92 | try { 93 | cmdStream.close(); 94 | } catch (IOException e) { 95 | Timber.e(e); 96 | } 97 | } 98 | } 99 | 100 | } 101 | 102 | if (exitCode == null && !isDestroyed) exitCode = 0; 103 | 104 | // The processor isn't running anymore so no more output/errors 105 | try { 106 | dataStream.close(); 107 | errorStream.close(); 108 | } catch (IOException e) { Timber.e(e); } 109 | 110 | exitLatch.countDown(); 111 | Timber.v("Processor finished."); 112 | }); 113 | processor.start(); 114 | } 115 | 116 | @Nullable 117 | public synchronized String getLastCommandRaw() { 118 | return cmdLines.poll(); 119 | } 120 | 121 | @Override 122 | public synchronized OutputStream getOutputStream() { 123 | return cmdStream; 124 | } 125 | 126 | @Override 127 | public synchronized InputStream getInputStream() { 128 | return dataStream; 129 | } 130 | 131 | @Override 132 | public synchronized InputStream getErrorStream() { 133 | return errorStream; 134 | } 135 | 136 | @Override 137 | public int waitFor() throws InterruptedException { 138 | Timber.v("waitFor()"); 139 | exitLatch.await(); 140 | return exitCode; 141 | } 142 | 143 | @Override 144 | public synchronized int exitValue() { 145 | if (exitCode == null || exitLatch.getCount() > 0) throw new IllegalThreadStateException(); 146 | else return exitCode; 147 | } 148 | 149 | @Override 150 | public boolean isAlive() { 151 | return exitLatch.getCount() > 0; 152 | } 153 | 154 | @Override 155 | public void destroy() { 156 | synchronized (this) { 157 | if (isDestroyed) return; 158 | isDestroyed = true; 159 | Timber.v("destroy()"); 160 | } 161 | 162 | if (exitCode == null) exitCode = RxProcess.ExitCode.PROBLEM; 163 | try { 164 | cmdStream.close(); 165 | dataStream.close(); 166 | errorStream.close(); 167 | } catch (IOException e) { 168 | Timber.e(e); 169 | } 170 | exitLatch.countDown(); 171 | } 172 | 173 | @Override 174 | public Process destroyForcibly() { 175 | destroy(); 176 | return this; 177 | } 178 | 179 | public void printData(String output) { 180 | dataStream.queue(output + LineReader.getLineSeparator()); 181 | } 182 | 183 | public void printError(String error) { 184 | errorStream.queue(error + LineReader.getLineSeparator()); 185 | } 186 | 187 | public void addCmdListener(CmdListener listener) { 188 | cmdListeners.add(listener); 189 | } 190 | 191 | public interface CmdListener { 192 | boolean onNewCmd(String line); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /core/src/test/java/testtools/MockRxShellSession.java: -------------------------------------------------------------------------------- 1 | package testtools; 2 | 3 | import java.io.IOException; 4 | import java.util.concurrent.LinkedBlockingQueue; 5 | 6 | import eu.darken.rxshell.shell.LineReader; 7 | import eu.darken.rxshell.shell.RxShell; 8 | import io.reactivex.rxjava3.core.Completable; 9 | import io.reactivex.rxjava3.core.Single; 10 | import io.reactivex.rxjava3.processors.PublishProcessor; 11 | import io.reactivex.rxjava3.processors.ReplayProcessor; 12 | import timber.log.Timber; 13 | 14 | import static org.mockito.ArgumentMatchers.any; 15 | import static org.mockito.ArgumentMatchers.anyBoolean; 16 | import static org.mockito.Mockito.doAnswer; 17 | import static org.mockito.Mockito.mock; 18 | import static org.mockito.Mockito.when; 19 | 20 | public class MockRxShellSession { 21 | PublishProcessor outputPub; 22 | PublishProcessor errorPub; 23 | ReplayProcessor waitForPub = ReplayProcessor.create(); 24 | LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); 25 | final RxShell.Session session; 26 | 27 | public MockRxShellSession() throws IOException { 28 | Thread thread = new Thread(() -> { 29 | while (true) { 30 | String line; 31 | try { 32 | line = queue.take(); 33 | } catch (InterruptedException e) { 34 | Timber.e(e); 35 | return; 36 | } 37 | if (line.endsWith(" $?")) { 38 | // By default we assume all commands exit OK 39 | final String[] split = line.split(" "); 40 | outputPub.onNext(split[1] + " " + 0); 41 | } else if (line.endsWith(" >&2")) { 42 | final String[] split = line.split(" "); 43 | errorPub.onNext(split[1]); 44 | } else if (line.startsWith("sleep")) { 45 | final String[] split = line.split(" "); 46 | long delay = Long.parseLong(split[1]); 47 | Timber.v("Sleeping for %d", delay); 48 | TestHelper.sleep(delay); 49 | } else if (line.startsWith("echo")) { 50 | final String[] split = line.split(" "); 51 | outputPub.onNext(split[1]); 52 | } else if (line.startsWith("error")) { 53 | final String[] split = line.split(" "); 54 | errorPub.onNext(split[1]); 55 | } else if (line.startsWith("exit")) { 56 | break; 57 | } 58 | } 59 | outputPub.onComplete(); 60 | errorPub.onComplete(); 61 | waitForPub.onNext(0); 62 | waitForPub.onComplete(); 63 | }); 64 | thread.start(); 65 | session = mock(RxShell.Session.class); 66 | 67 | doAnswer(invocation -> { 68 | String line = invocation.getArgument(0); 69 | boolean flush = invocation.getArgument(1); 70 | Timber.d("writeLine(%s, %b)", line, flush); 71 | queue.add(line); 72 | return null; 73 | }).when(session).writeLine(any(), anyBoolean()); 74 | 75 | outputPub = PublishProcessor.create(); 76 | when(session.outputLines()).thenReturn(outputPub); 77 | 78 | errorPub = PublishProcessor.create(); 79 | when(session.errorLines()).thenReturn(errorPub); 80 | 81 | when(session.cancel()).then(invocation -> Completable.create(e -> { 82 | Timber.i("cancel()"); 83 | thread.interrupt(); 84 | 85 | outputPub.onComplete(); 86 | errorPub.onComplete(); 87 | 88 | waitForPub.onNext(1); 89 | waitForPub.onComplete(); 90 | e.onComplete(); 91 | })); 92 | 93 | Single close = Completable.create(e -> { 94 | queue.add("exit" + LineReader.getLineSeparator()); 95 | e.onComplete(); 96 | }).andThen(waitForPub.lastOrError()).cache(); 97 | when(session.close()).thenReturn(close); 98 | 99 | waitForPub.doOnEach(integerNotification -> Timber.i("waitFor %s", integerNotification)).subscribe(); 100 | 101 | when(session.waitFor()).thenReturn(waitForPub.lastOrError()); 102 | when(session.isAlive()).thenReturn(Single.create(e -> e.onSuccess(thread.isAlive()))); 103 | } 104 | 105 | public RxShell.Session getSession() { 106 | return session; 107 | } 108 | 109 | public PublishProcessor getErrorPub() { 110 | return errorPub; 111 | } 112 | 113 | public PublishProcessor getOutputPub() { 114 | return outputPub; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /core/src/test/java/testtools/StreamHelper.java: -------------------------------------------------------------------------------- 1 | package testtools; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.InputStream; 5 | import java.nio.charset.Charset; 6 | 7 | public class StreamHelper { 8 | public static InputStream makeStream(String string) { 9 | return new ByteArrayInputStream(string.getBytes(Charset.defaultCharset())); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/test/java/testtools/TestHelper.java: -------------------------------------------------------------------------------- 1 | package testtools; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 8 | 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.core.Is.is; 11 | 12 | public class TestHelper { 13 | public static void sleep(long millis) { 14 | try { 15 | Thread.sleep(millis); 16 | } catch (InterruptedException e) { 17 | e.printStackTrace(); 18 | } 19 | } 20 | 21 | public static List trackPluginErrors() { 22 | final List list = Collections.synchronizedList(new ArrayList()); 23 | RxJavaPlugins.setErrorHandler(list::add); 24 | return list; 25 | } 26 | 27 | public static void assertTODO() { 28 | assertThat("TODO", true, is(false)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileOptions { 5 | sourceCompatibility versions.sourceCompatibility 6 | targetCompatibility versions.targetCompatibility 7 | } 8 | 9 | compileSdkVersion versions.compileSdk 10 | 11 | defaultConfig { 12 | applicationId "eu.darken.rxshell.example" 13 | 14 | minSdkVersion versions.minSdk 15 | targetSdkVersion versions.targetSdk 16 | 17 | versionCode versions.versionCode 18 | versionName versions.versionName 19 | 20 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | 30 | } 31 | dependencies { 32 | implementation project(':core') 33 | implementation project(':root') 34 | 35 | implementation 'com.android.support.constraint:constraint-layout:1.1.2' 36 | implementation deps.support.appcompat 37 | 38 | implementation 'com.jakewharton:butterknife:8.8.1' 39 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 40 | 41 | implementation deps.timber 42 | 43 | implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' 44 | 45 | testImplementation deps.jUnit 46 | } -------------------------------------------------------------------------------- /example/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /example/src/main/java/eu/darken/rxshellexample/App.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshellexample; 2 | 3 | import android.app.Application; 4 | 5 | import eu.darken.rxshell.extra.RXSDebug; 6 | import timber.log.Timber; 7 | 8 | public class App extends Application { 9 | @Override 10 | public void onCreate() { 11 | super.onCreate(); 12 | 13 | RXSDebug.addCallback(new RXSDebug.ProcessCallback() { 14 | @Override 15 | public void onProcessStart(Process process) { 16 | Timber.i("Process started: %s", process); 17 | } 18 | 19 | @Override 20 | public void onProcessEnd(Process process) { 21 | Timber.i("Process ended: %s", process); 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/src/main/java/eu/darken/rxshellexample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package eu.darken.rxshellexample; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.View; 6 | import android.widget.Button; 7 | import android.widget.TextView; 8 | 9 | import butterknife.BindView; 10 | import butterknife.ButterKnife; 11 | import butterknife.OnClick; 12 | import eu.darken.rxshell.cmd.Cmd; 13 | import eu.darken.rxshell.cmd.RxCmdShell; 14 | import eu.darken.rxshell.root.RootContext; 15 | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; 16 | import io.reactivex.rxjava3.core.Single; 17 | import io.reactivex.rxjava3.schedulers.Schedulers; 18 | import timber.log.Timber; 19 | 20 | public class MainActivity extends AppCompatActivity { 21 | @BindView(R.id.output) TextView output; 22 | @BindView(R.id.input) TextView input; 23 | @BindView(R.id.execute) Button execute; 24 | @BindView(R.id.root_result) TextView rootResult; 25 | private RxCmdShell.Session session; 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | Timber.plant(new Timber.DebugTree()); 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_main); 32 | ButterKnife.bind(this); 33 | execute.setVisibility(View.INVISIBLE); 34 | } 35 | 36 | @Override 37 | protected void onResume() { 38 | super.onResume(); 39 | RxCmdShell rxCommandShell = RxCmdShell.builder().build(); 40 | rxCommandShell.open() 41 | .observeOn(AndroidSchedulers.mainThread()) 42 | .subscribe(session -> { 43 | execute.setVisibility(View.VISIBLE); 44 | MainActivity.this.session = session; 45 | }); 46 | } 47 | 48 | @OnClick(R.id.execute) 49 | public void onExecute(View v) { 50 | Cmd.builder(input.getText().toString()).submit(session) 51 | .observeOn(AndroidSchedulers.mainThread()) 52 | .subscribe(result -> { 53 | output.setText(result.getCmd().toString()); 54 | output.append("\n\n"); 55 | 56 | for (String o : result.merge()) output.append(o + "\n"); 57 | 58 | output.append("\n"); 59 | output.append(result.toString()); 60 | }); 61 | } 62 | 63 | @OnClick(R.id.check_root) 64 | public void onCheckRoot(View v) { 65 | final Single rootContextSingle = new RootContext.Builder(getApplicationContext()).build(); 66 | rootContextSingle.subscribeOn(Schedulers.io()) 67 | .observeOn(AndroidSchedulers.mainThread()) 68 | .subscribe(rootContext -> rootResult.setText(String.format("Root-State: %s", rootContext.getRoot().getState()))); 69 | } 70 | 71 | @Override 72 | protected void onPause() { 73 | super.onPause(); 74 | if (session != null) { 75 | session.close() 76 | .doOnSubscribe(d -> session = null) 77 | .observeOn(AndroidSchedulers.mainThread()) 78 | .subscribe(i -> execute.setVisibility(View.INVISIBLE)); 79 | } 80 | } 81 | 82 | @Override 83 | protected void onDestroy() { 84 | Timber.uprootAll(); 85 | super.onDestroy(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /example/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 |