Git Product home page Git Product logo

ktsh's Introduction

KtSh

An open source library to execute shell commands on Android or the JVM, written in Kotlin.

License Build Status Maven Central

Downloads

Download the latest JAR or grab via Gradle:

implementation 'com.jaredrummler:ktsh:1.0.0'

Alternatively, you can simply copy Shell.kt file to your project.

Usage

Basic usage:

val shell = Shell("sh")                         // create a shell
val result = shell.run("echo 'Hello, World!'")  // execute a command
if (result.isSuccess) {                         // check if the exit-code was 0
    println(result.stdout())                    // prints "Hello, World!"
}

Construct a new Shell instance:

// Construct a new shell instance with additional environment variables
val shell = Shell("sh", "USER" to "Chuck Norris", "ENV_VAR" to "VALUE")

// Construct a new shell instance with path to the shell:
val bash = Shell("/bin/bash")

Note: If the shell does not exist a Shell.NotFoundException is thrown as a RuntimeException.

Execute a command and get the result:

val shell = Shell.SH
val result: Shell.Command.Result = shell.run("ls")

A Shell.Command.Result contains the following:

  • stdout: A list of lines read from the standard input stream.
  • stderr: A list of lines read from the standard error stream.
  • exitCode: The exit status from running the command.
  • details: Additional information (start, stop, elapsed time, id, command)

Add a callback when stdout or stderr is read:

shell.addOnStderrLineListener(object : Shell.OnLineListener {
  override fun onLine(line: String) {
      // do something
  }
})

Add a callback that is invoked each time a command completes:

shell.addOnCommandResultListener(object : Shell.OnCommandResultListener {
  override fun onResult(result: Shell.Command.Result) {
    // do something with the result
  }
})

Execute a command with custom options:

Optionally, you can configure how each command executes by setting a timeout, redirecting stderr to stdout, add callbacks for when the command reads a line from stdout/stderr or is cancelled.

// NOTE: all of these are optional
val result = Shell.SH.run(command) {
  // Kill the command after 1 minute
  timeout = Shell.Timeout(1, TimeUnit.MINUTES)
  // Redirect STDOUT to STDERR
  redirectErrorStream = false
  // Callbacks:
  onCancelled = {
    // The command was cancelled
  }
  onStdErr = { line: String ->
    // Do something when reading a line from standard error stream
  }
  onStdOut = { line: String ->
    // Do something when reading a line from standard output stream
  }
  // Do not notify any listeners added via Shell.addOnStderrLineListener and Shell.addOnStdoutLineListener
  notify = false
}

Check the state of the shell:

if (shell.isRunning()) {
  // The shell is running a command
} else if (shell.isShutdown()) {
  // The shell has been killed
} else if (shell.isIdle()) {
  // The shell is open and not running any commands
}

or

when (shell.state) {
  State.Idle -> TODO()
  State.Running -> TODO()
  State.Shutdown -> TODO()
}

Shutdown the shell

shell.shutdown()

Interrupt waiting for a command to complete:

shell.interrupt()

Background processing on Android

Creating a new instance of a Shell or executing commands should be done on a separate thread other than the UI thread. This is up to the library user. An example of this can be found in the demo project using Kotlin coroutines and AndroidX libraries:

fun run(
    shell: Shell,
    command: String,
    callback: (result: Shell.Command.Result) -> Unit
) = viewModelScope.launch {
    val result = withContext(Dispatchers.IO) { shell.run(command) }
    withContext(Dispatchers.Main) { callback(result) }
}

Structure

  • buildSrc - contains dependencies, plugins, versions for Gradle build logic
  • build.gradle.kts - root gradle config file
  • settings.gradle.kts - root gradle settings file
  • library - the ktsh library
  • library/src/test - unit tests for the library
  • demo - Android demo project using ktsh
  • scripts - scripts to publish library to maven central
  • .github - any files for the github page

Similar projects:

License

Copyright (C) 2021 Jared Rummler

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

ktsh's People

Contributors

jaredrummler avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ktsh's Issues

Shell.Builder Class

Please provide an example that how to use Shell.builder() class, and how to reset shell
or How to use multiple commands at once using this library.

OnCommandResultListener.onCommandResult() is called prematurely

I'm using AndroidShell for an rsync app, and almost immediately after invoking rsync, the OnCommandResultListener that I pass to Shell.Builder.open() has its onCommandResult() called. The command can continue running for several minutes thereafter. I see the same behavior if I run "top" instead of "rsync" so it doesn't appear to be tied to any specific long-running command.

Can we have a wiki?

i took me quite a bit of a time and effort to figure out, how to execute command with root permission(figured out at last by reading closed issue). Since i tried su along with my command, however my command wont execute due to permissioin issue. Had to use Shell.SU
I can contribute, as i figure things out.

Continuesly reading command output

First, thank you for this valuable lib!

I want to run a command, keep it running in the background and read the from stdout/stderr.

Is this something I can do with this libs? I have been looking at the code but I couldn't find out how to do that.

How to set Device Owner

How can I execute below permissions

val result = Shell.SH.run("adb shell dpm set-device-owner com.app.test/.AppAdminReceiver") as CommandResult

Run from .sh file

Can I execute .sh files? If yes, can you link me to that guide? Thanks!

Question: Interacting with the script/command

Greetings!
Thanks for creating this great library!
as the title itself, this is not a issue or a bug but a question regarding on how to interact with the script/command

command/script:
echo "How old are you?"
read -p age
echo "so your age is "$age

2 scenario:

Scenario A:(putting it on shell script (*.sh file))
Steps:

  • copy the script to /data/data/package_name/home
  • adding proper permission using chmod
  • running the script
    Result:
    it will stdout/err at once:(something looks like below)
    How old are you?
    read rand_id
    so your age is age_rand_id
    (random generated id/text = rand_id)
    and when you submit another command it will not stdout/err or any output

Scenario 2:(adding the command libe by line)
from the start, executing echo will work : "How old are you?"
but executing read command it will not display any output or stdout/err...
and when you submit another command it will not stdout/err or any output

Big question: how should I tackle this?

and I also notice this in some unfinished command and for loop command(assuming that you will put the command line by line), and it seems you partly added the behaviour that I want (interact with script) but it seems it only works in if-else statement.

Lib_ver: the current/latest jar as of june 2023
Phone: android 9, aarch64,Armv8a, not-rooted

Thanks and regards,
STICKnoLOGIC

Exception in thread "main" com.jaredrummler.ktsh.Shell$NotFoundException on Windows

I'm not sure if I'm doing something wrong here, but I got this error on a simple kotlin project.

Exception in thread "main" com.jaredrummler.ktsh.Shell$NotFoundException: Error opening shell: 'sh'
	at com.jaredrummler.ktsh.Shell.<init>(Shell.kt:95)
	at com.jaredrummler.ktsh.Shell.<init>(Shell.kt:52)
	at com.jaredrummler.ktsh.Shell$Companion.get(Shell.kt:761)
	at com.jaredrummler.ktsh.Shell$Companion.getSH(Shell.kt:766)
	at MainKt.main(Main.kt:4)
	at MainKt.main(Main.kt)
Caused by: java.io.IOException: Cannot run program "sh": CreateProcess error=2, The system cannot find the file specified
	at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1143)
	at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1073)
	at java.base/java.lang.Runtime.exec(Runtime.java:615)
	at java.base/java.lang.Runtime.exec(Runtime.java:439)
	at java.base/java.lang.Runtime.exec(Runtime.java:370)
	at com.jaredrummler.ktsh.Shell$Companion.runWithEnv(Shell.kt:784)
	at com.jaredrummler.ktsh.Shell$Companion.access$runWithEnv(Shell.kt:746)
	at com.jaredrummler.ktsh.Shell.<init>(Shell.kt:90)
	... 5 more
Caused by: java.io.IOException: CreateProcess error=2, The system cannot find the file specified
	at java.base/java.lang.ProcessImpl.create(Native Method)
	at java.base/java.lang.ProcessImpl.<init>(ProcessImpl.java:494)
	at java.base/java.lang.ProcessImpl.start(ProcessImpl.java:159)
	at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1110)
	... 12 more

Process finished with exit code 1
import com.jaredrummler.ktsh.Shell

fun main() {
    val shell = Shell.SH                       // create a shell
    val result = shell.run("echo 'Hello, World!'")  // execute a command
    if (result.isSuccess) {                         // check if the exit-code was 0
        println(result.stdout())                    // prints "Hello, World!"
    }
}

Expose shell.state variable

Shell state is private but it would be useful to add an accessor for use in a when statement (as shown in the docs).

Basic Usage returns me permission denied

Hi @jaredrummler,

I'm trying this command:

val shell = Shell("sh")  // or sh -l
val result = shell.run("ls") 
if (result.isSuccess) {
    println(result.stdout())
}

and it's returning me on the stderr an error of permission denied.

Whats wrong?

thank you

MessageQueue: Handler (android.os.Handler) {cdcc738} sending message to a Handler on a dead thread

11-28 07:00:33.699 W/art: Long monitor contention event with owner method=int libcore.io.Posix.readBytes(java.io.FileDescriptor, java.lang.Object, int, int) from Posix.java:4294967294 waiters=0 for 14.898s
11-28 07:00:33.705 W/art: Long monitor contention event with owner method=void java.io.FileInputStream.close() from FileInputStream.java:114 waiters=0 for 14.904s
11-28 07:00:33.705 W/art: Long monitor contention event with owner method=void java.io.FileInputStream.close() from FileInputStream.java:114 waiters=1 for 14.838s
11-28 07:00:33.706 W/art: Long monitor contention event with owner method=void java.lang.Object.wait!() from Object.java:4294967294 waiters=0 for 31.268s
11-28 07:00:33.723 W/MessageQueue: Handler (android.os.Handler) {cdcc738} sending message to a Handler on a dead thread
    java.lang.IllegalStateException: Handler (android.os.Handler) {cdcc738} sending message to a Handler on a dead thread
        at android.os.MessageQueue.enqueueMessage(MessageQueue.java:325)
        at android.os.Handler.enqueueMessage(Handler.java:631)
        at android.os.Handler.sendMessageAtTime(Handler.java:600)
        at android.os.Handler.sendMessageDelayed(Handler.java:570)
        at android.os.Handler.post(Handler.java:326)
        at com.jaredrummler.android.shell.Shell$Interactive.postCallback(Shell.java:1383)
        at com.jaredrummler.android.shell.Shell$Interactive.runNextCommand(Shell.java:1289)
        at com.jaredrummler.android.shell.Shell$Interactive.runNextCommand(Shell.java:1178)
        at com.jaredrummler.android.shell.Shell$Interactive.addCommand(Shell.java:1151)
        at com.jaredrummler.android.shell.Shell$Console.run(Shell.java:1761)
        at com.jaredrummler.android.shell.Shell$SU.run(Shell.java:374)

unable to set HOME in root shell environment

Using Shell.Builder(), I can set HOME via addEnvironment() when I call useSH() but not when I call useSU(). I tested by invoking "env" as the shell command.

The same value I'm trying to assign to HOME can be assigned to different keys, so it's not a problem with the value itself. I can also set HOME in a root shell in adb.

I tried this in Android 4.4 and 8.0.0, and I assume any version in the middle is the same way.

how can I check I got su permission

I use supersu to manager su request.

And how can I check I have already got su permission.

Sorry for my poor English. Any point need explain?

commandResult.exitCode return -1

I am trying to get hash value using MD5sum using Shell.SU.run() method.
but the result always returns -1, and isSuccessful() method not called.

       var result:String? = null
        val hashCommand = Shell.SU.run("md5sum $path")
        Log.d("getHash", "Path : $path, Before : " + hashCommand.stdout.toString() + ", Error : " + hashCommand.stderr + ", Exit Code : " + hashCommand.exitCode)
        if (hashCommand.isSuccessful) {
            result =  hashCommand.stdout.toString()
            Log.d("getHash", "Path : $path, Success : " + hashCommand.stdout.toString() + ", Error : " + hashCommand.stderr)
        }
        Log.d("getHash", "Path : $path, Last : " + hashCommand.stdout.toString() + ", Error : " + hashCommand.stderr + ", Exit Code : " + hashCommand.exitCode)
       

RxJava Backends

Request to have RxJava backends, so that we can asynchronously run a pipeline of commands. Is this possible?

Command that needs WRITE_SECURE_SETTINGS permission won't work even if permission is granted

From other apps I know that they can set the screen size (which is done via wm overscan <LEFT>,<TOP>,<RIGHT>,<BOTTOM> from within the app after you have granted android.permission.WRITE_SECURE_SETTINGS to those apps.

Any ideas how I can "forward" this permission to the shell? I thought the shell will have the same permission and UID as my app, but I can't get to work what other apps can do.

My simple test case looks like following:

val test: (Int, String) -> Unit = { index, command ->
	val res = Shell.SH.run(command)
	L.d { "Result $index: ${res.isSuccessful} | ${res.exitCode} | ${res.getStdout()} | ${res.getStderr()}" }
}

val commands = arrayOf(
		"wm overscan 0,0,0,$overscanOffset",
		"settings get global policy_control",
		"echo \$PATH",
		"echo Some echod text",
		"id"
)
var c = 1
for (command in commands) {
	test(c++, command)
}
Shell.SH.closeConsole()

And the output is following:

2019-03-12 08:53:13.682 D/[UITuner$setOverscanStatus$test:56 invoke]: Result 1: false | 20 |  | cmd: Can't find service: window (UITuner.kt:56)
2019-03-12 08:53:13.714 D/[UITuner$setOverscanStatus$test:56 invoke]: Result 2: false | 255 |  | Security exception: You either need MANAGE_USERS or CREATE_USERS permission to: query user (UITuner.kt:56)
    
    java.lang.SecurityException: You either need MANAGE_USERS or CREATE_USERS permission to: query user
        at com.android.server.pm.UserManagerService.checkManageOrCreateUsersPermission(UserManagerService.java:2097)
        at com.android.server.pm.UserManagerService.getUserInfo(UserManagerService.java:1250)
        at android.os.UserManager.getUserInfo(UserManager.java:1636)
        at com.android.providers.settings.SettingsService$MyShellCommand.onCommand(SettingsService.java:273)
        at android.os.ShellCommand.exec(ShellCommand.java:103)
        at com.android.providers.settings.SettingsService.onShellCommand(SettingsService.java:51)
        at android.os.Binder.shellCommand(Binder.java:642)
        at android.os.Binder.onTransact(Binder.java:540)
        at android.os.Binder.execTransact(Binder.java:739)
2019-03-12 08:53:13.727 D/[UITuner$setOverscanStatus$test:56 invoke]: Result 3: true | 0 | /sbin:/system/sbin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin |  (UITuner.kt:56)
2019-03-12 08:53:13.741 D/[UITuner$setOverscanStatus$test:56 invoke]: Result 4: true | 0 | Some echod text |  (UITuner.kt:56)
2019-03-12 08:53:13.764 D/[UITuner$setOverscanStatus$test:56 invoke]: Result 5: true | 0 | uid=10290(u0_a290) gid=10290(u0_a290) groups=10290(u0_a290),9997(everybody),20290(u0_a290_cache),50290(all_a290) context=u:r:untrusted_app:s0:c34,c257,c512,c768 |  (UITuner.kt:56)

I'm not sure if test 2 can succeed (have not seen an app doing it yet), but test 1 can succeed on unrooted devices if android.permission.WRITE_SECURE_SETTINGS is granted to the app.

Do you have any ideas, why I instead get the Can't find service: window although the same command works perfectly fine from an adb command line? I would expect an exception, if a permission is missing, but not the can't find error... I'm not sure at all if WRITE_SECURE_SETTINGS and shell commands can work together at all, any ideas if this is even possible?

Sample apk

From where i can download sample apk?

onStdOut not properly working

Btw as mentioned in an issue#14
app is not responsive until command finishes and ping just continues to run

I'm using in jetpack compose as this

var test by remember { mutableStateOf("initial test string") }
Text(text = test)
LaunchedEffect(Unit) {
Shell.SU.run("ping google.com") {
onStdOut = { line: String ->
test = line
}
timeout = Shell.Timeout(1, TimeUnit.MILLISECONDS)
}
}

and the issue is unless I specify timeout, shell doesn't return anything

pm unistall is not working

Remove system app is failed
CommandResult result = Shell.SU.run("pm uninstall " + ai.packageName);
return "Failed" on rooted device

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.