js: support spawn jsapi
This commit is contained in:
71
js/README.md
71
js/README.md
@@ -10,10 +10,10 @@ yarn add kernelsu
|
|||||||
|
|
||||||
### exec
|
### exec
|
||||||
|
|
||||||
Execute a command in the **root** shell.
|
Spawns a **root** shell and runs a command within that shell, passing the `stdout` and `stderr` to a Promise when complete.
|
||||||
|
|
||||||
options:
|
|
||||||
|
|
||||||
|
- `command` `<string>` The command to run, with space-separated arguments.
|
||||||
|
- `options` `<Object>`
|
||||||
- `cwd` - Current working directory of the child process
|
- `cwd` - Current working directory of the child process
|
||||||
- `env` - Environment key-value pairs
|
- `env` - Environment key-value pairs
|
||||||
|
|
||||||
@@ -27,6 +27,71 @@ if (errno === 0) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### spawn
|
||||||
|
|
||||||
|
Spawns a new process using the given `command` in **root** shell, with command-line arguments in `args`. If omitted, `args` defaults to an empty array.
|
||||||
|
|
||||||
|
Returns a `ChildProcess`, Instances of the ChildProcess represent spawned child processes.
|
||||||
|
|
||||||
|
- `command` `<string>` The command to run.
|
||||||
|
- `args` `<string[]>` List of string arguments.
|
||||||
|
- `options` `<Object>`:
|
||||||
|
- `cwd` `<string>` - Current working directory of the child process
|
||||||
|
- `env` `<Object>` - Environment key-value pairs
|
||||||
|
|
||||||
|
Example of running `ls -lh /data`, capturing `stdout`, `stderr`, and the exit code:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { spawn } from 'kernelsu';
|
||||||
|
|
||||||
|
const ls = spawn('ls', ['-lh', '/data']);
|
||||||
|
|
||||||
|
ls.stdout.on('data', (data) => {
|
||||||
|
console.log(`stdout: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
ls.stderr.on('data', (data) => {
|
||||||
|
console.log(`stderr: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
ls.on('exit', (code) => {
|
||||||
|
console.log(`child process exited with code ${code}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ChildProcess
|
||||||
|
|
||||||
|
##### Event 'exit'
|
||||||
|
|
||||||
|
- `code` `<number>` The exit code if the child exited on its own.
|
||||||
|
|
||||||
|
The `'exit'` event is emitted after the child process ends. If the process exited, `code` is the final exit code of the process, otherwise null
|
||||||
|
|
||||||
|
##### Event 'error'
|
||||||
|
|
||||||
|
- `err` `<Error>` The error.
|
||||||
|
|
||||||
|
The `'error'` event is emitted whenever:
|
||||||
|
|
||||||
|
- The process could not be spawned.
|
||||||
|
- The process could not be killed.
|
||||||
|
|
||||||
|
##### `stdout`
|
||||||
|
|
||||||
|
A `Readable Stream` that represents the child process's `stdout`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const subprocess = spawn('ls');
|
||||||
|
|
||||||
|
subprocess.stdout.on('data', (data) => {
|
||||||
|
console.log(`Received chunk ${data}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `stderr`
|
||||||
|
|
||||||
|
A `Readable Stream` that represents the child process's `stderr`.
|
||||||
|
|
||||||
### fullScreen
|
### fullScreen
|
||||||
|
|
||||||
Request the WebView enter/exit full screen.
|
Request the WebView enter/exit full screen.
|
||||||
|
|||||||
79
js/index.js
79
js/index.js
@@ -1,6 +1,6 @@
|
|||||||
let callbackCounter = 0;
|
let callbackCounter = 0;
|
||||||
function getUniqueCallbackName() {
|
function getUniqueCallbackName(prefix) {
|
||||||
return `_callback_${Date.now()}_${callbackCounter++}`;
|
return `${prefix}_callback_${Date.now()}_${callbackCounter++}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exec(command, options) {
|
export function exec(command, options) {
|
||||||
@@ -10,7 +10,7 @@ export function exec(command, options) {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Generate a unique callback function name
|
// Generate a unique callback function name
|
||||||
const callbackFuncName = getUniqueCallbackName();
|
const callbackFuncName = getUniqueCallbackName("exec");
|
||||||
|
|
||||||
// Define the success callback function
|
// Define the success callback function
|
||||||
window[callbackFuncName] = (errno, stdout, stderr) => {
|
window[callbackFuncName] = (errno, stdout, stderr) => {
|
||||||
@@ -23,11 +23,7 @@ export function exec(command, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ksu.exec(
|
ksu.exec(command, JSON.stringify(options), callbackFuncName);
|
||||||
command,
|
|
||||||
JSON.stringify(options),
|
|
||||||
callbackFuncName,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
cleanup(callbackFuncName);
|
cleanup(callbackFuncName);
|
||||||
@@ -35,6 +31,73 @@ export function exec(command, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Stdio() {
|
||||||
|
this.listeners = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Stdio.prototype.on = function (event, listener) {
|
||||||
|
if (!this.listeners[event]) {
|
||||||
|
this.listeners[event] = [];
|
||||||
|
}
|
||||||
|
this.listeners[event].push(listener);
|
||||||
|
};
|
||||||
|
|
||||||
|
Stdio.prototype.emit = function (event, ...args) {
|
||||||
|
if (this.listeners[event]) {
|
||||||
|
this.listeners[event].forEach((listener) => listener(...args));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function ChildProcess() {
|
||||||
|
this.listeners = {};
|
||||||
|
this.stdin = new Stdio();
|
||||||
|
this.stdout = new Stdio();
|
||||||
|
this.stderr = new Stdio();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChildProcess.prototype.on = function (event, listener) {
|
||||||
|
if (!this.listeners[event]) {
|
||||||
|
this.listeners[event] = [];
|
||||||
|
}
|
||||||
|
this.listeners[event].push(listener);
|
||||||
|
};
|
||||||
|
|
||||||
|
ChildProcess.prototype.emit = function (event, ...args) {
|
||||||
|
if (this.listeners[event]) {
|
||||||
|
this.listeners[event].forEach((listener) => listener(...args));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function spawn(command, args, options) {
|
||||||
|
if (typeof args === "undefined") {
|
||||||
|
args = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options === "undefined") {
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = new ChildProcess();
|
||||||
|
const childCallbackName = getUniqueCallbackName("spawn");
|
||||||
|
window[childCallbackName] = child;
|
||||||
|
|
||||||
|
function cleanup(name) {
|
||||||
|
delete window[name];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ksu.spawn(
|
||||||
|
command,
|
||||||
|
JSON.stringify(args),
|
||||||
|
JSON.stringify(options),
|
||||||
|
childCallbackName
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
child.emit("error", error);
|
||||||
|
cleanup(childCallbackName);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
export function fullScreen(isFullScreen) {
|
export function fullScreen(isFullScreen) {
|
||||||
ksu.fullScreen(isFullScreen);
|
ksu.fullScreen(isFullScreen);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ import com.google.accompanist.web.WebView
|
|||||||
import com.google.accompanist.web.rememberWebViewState
|
import com.google.accompanist.web.rememberWebViewState
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.topjohnwu.superuser.CallbackList
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import me.weishu.kernelsu.ui.util.createRootShell
|
import me.weishu.kernelsu.ui.util.createRootShell
|
||||||
import me.weishu.kernelsu.ui.util.serveModule
|
import me.weishu.kernelsu.ui.util.serveModule
|
||||||
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@Destination
|
@Destination
|
||||||
@@ -83,6 +86,23 @@ class WebViewInterface(val context: Context, val webView: WebView) {
|
|||||||
exec(cmd, null, callbackFunc)
|
exec(cmd, null, callbackFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun processOptions(sb: StringBuilder, options: String?) {
|
||||||
|
val opts = if (options == null) JSONObject() else {
|
||||||
|
JSONObject(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cwd = opts.optString("cwd")
|
||||||
|
if (!TextUtils.isEmpty(cwd)) {
|
||||||
|
sb.append("cd ${cwd};")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.optJSONObject("env")?.let { env ->
|
||||||
|
env.keys().forEach { key ->
|
||||||
|
sb.append("export ${key}=${env.getString(key)};")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun exec(
|
fun exec(
|
||||||
cmd: String,
|
cmd: String,
|
||||||
@@ -94,18 +114,7 @@ class WebViewInterface(val context: Context, val webView: WebView) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val finalCommand = StringBuilder()
|
val finalCommand = StringBuilder()
|
||||||
|
processOptions(finalCommand, options)
|
||||||
val cwd = opts.optString("cwd")
|
|
||||||
if (!TextUtils.isEmpty(cwd)) {
|
|
||||||
finalCommand.append("cd ${cwd};")
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.optJSONObject("env")?.let { env ->
|
|
||||||
env.keys().forEach { key ->
|
|
||||||
finalCommand.append("export ${key}=${env.getString(key)};")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalCommand.append(cmd)
|
finalCommand.append(cmd)
|
||||||
|
|
||||||
val shell = createRootShell()
|
val shell = createRootShell()
|
||||||
@@ -124,6 +133,78 @@ class WebViewInterface(val context: Context, val webView: WebView) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun spawn(command: String, args: String, options: String?, callbackFunc: String) {
|
||||||
|
val finalCommand = StringBuilder()
|
||||||
|
|
||||||
|
processOptions(finalCommand, options)
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(args)) {
|
||||||
|
finalCommand.append(command).append(" ")
|
||||||
|
JSONArray(args).let { argsArray ->
|
||||||
|
for (i in 0 until argsArray.length()) {
|
||||||
|
finalCommand.append(argsArray.getString(i))
|
||||||
|
finalCommand.append(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalCommand.append(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shell = createRootShell()
|
||||||
|
|
||||||
|
val emitData = fun(name: String, data: String) {
|
||||||
|
val jsCode =
|
||||||
|
"javascript: (function() { try { ${callbackFunc}.${name}.emit('data', ${
|
||||||
|
JSONObject.quote(
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}); } catch(e) { console.error('emitData', e); } })();"
|
||||||
|
webView.post {
|
||||||
|
webView.loadUrl(jsCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val stdout = object : CallbackList<String>() {
|
||||||
|
override fun onAddElement(s: String) {
|
||||||
|
emitData("stdout", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val stderr = object : CallbackList<String>() {
|
||||||
|
override fun onAddElement(s: String) {
|
||||||
|
emitData("stderr", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val future = shell.newJob().add(finalCommand.toString()).to(stdout, stderr).enqueue()
|
||||||
|
val completableFuture = CompletableFuture.supplyAsync {
|
||||||
|
future.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
completableFuture.thenAccept { result ->
|
||||||
|
val emitExitCode =
|
||||||
|
"javascript: (function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();"
|
||||||
|
webView.post {
|
||||||
|
webView.loadUrl(emitExitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
val emitErrCode =
|
||||||
|
"javascript: (function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${
|
||||||
|
JSONObject.quote(
|
||||||
|
result.err.joinToString(
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};${callbackFunc}.emit('error', err); } catch(e) { console.error('emitErr', e); } })();"
|
||||||
|
webView.post {
|
||||||
|
webView.loadUrl(emitErrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun toast(msg: String) {
|
fun toast(msg: String) {
|
||||||
webView.post {
|
webView.post {
|
||||||
|
|||||||
Reference in New Issue
Block a user