From 2056a2153855c4ed330a760e6c0838d050c6c926 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 16 Apr 2022 14:44:16 +0100 Subject: [PATCH 1/5] Add shell script execution to the server --- .../java/com/genymobile/scrcpy/Command.java | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/Command.java index 0ef976a6..cd5bb89b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Command.java +++ b/server/src/main/java/com/genymobile/scrcpy/Command.java @@ -1,7 +1,9 @@ package com.genymobile.scrcpy; -import java.io.IOException; +import java.io.*; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Scanner; public final class Command { @@ -17,6 +19,44 @@ public final class Command { } } + public static void execShellScript(String script, String... args) throws IOException, InterruptedException { + + ArrayList cmd = new ArrayList<>(); + cmd.add("sh"); + cmd.add("-s"); + cmd.addAll(Arrays.asList(args)); + + Process process = Runtime.getRuntime().exec(cmd.toArray(new String[]{})); + BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream())); + BufferedReader output = new BufferedReader(new InputStreamReader(process.getInputStream())); + BufferedWriter input = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); + input.write(script); + input.close(); + + StringBuilder fullLines = new StringBuilder(); + String line; + if (Ln.isEnabled(Ln.Level.DEBUG)) { + while ((line = output.readLine()) != null) { + fullLines.append(line); + fullLines.append("\n"); + } + Ln.d("Custom script output:\n---\n" + fullLines + "\n----\n"); + } + fullLines = new StringBuilder(); + if (Ln.isEnabled(Ln.Level.WARN)) { + while ((line = err.readLine()) != null) { + fullLines.append(line); + fullLines.append("\n"); + } + Ln.w("Custom script err:\n---\n" + fullLines + "\n----\n"); + } + + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Custom script with args: " + Arrays.toString(args) + " returned with value " + exitCode); + } + } + public static String execReadLine(String... cmd) throws IOException, InterruptedException { String result = null; Process process = Runtime.getRuntime().exec(cmd); From 3e69f696c1f3579eacb199c5447348194aabf218 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 16 Apr 2022 14:45:51 +0100 Subject: [PATCH 2/5] Create: sc_str_find_replace() Find and replace all matches in a string --- app/src/util/str.c | 70 ++++++++++++++++++++++++++++++++++++++++++++++ app/src/util/str.h | 21 ++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/app/src/util/str.c b/app/src/util/str.c index d78aa9d7..b604f39e 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -48,6 +48,76 @@ truncated: return n; } +int64_t +sc_str_find_replace(char* input, char* find, char* replace, char** output) { + + uint64_t inputLen; + int64_t findLen = strlen(find); + int64_t replaceLen = strlen(replace); + + if(findLen < 1){ + return 0; + } + + + uint64_t first_match = -1; + uint32_t matchCount = 0; + + for(inputLen = 0; input[inputLen] != '\0'; inputLen++){ + // strncmp and not memcmp because it needs to detect null terminating string. Otherwise, it may overrrun. + if(input[inputLen] == find[0] && strncmp(&(input[inputLen]), find, findLen) == 0){ + if(first_match == (uint64_t) -1){ + first_match = inputLen; + } + matchCount++; + } + } + printf("matches: %u; \n", matchCount); + inputLen++; + if(matchCount == 0){ + return 0; + } + + int64_t output_size = inputLen - ( matchCount * (findLen - replaceLen)); + + if(output_size <= 0){ + return -2; + } + + // Caller is responsible for freeing this + char* output_str = malloc(output_size); + + if(output_str == NULL){ + printf("inputi: %p\n", output_str); + return -2; + } + *output = output_str; + + memcpy(output_str, input, first_match); + + uint64_t inputi = first_match; + uint64_t desti = first_match; + + while (input[inputi] != '\0') { + printf("inputi: %lu; %i; %lu; %lu; %s\n", inputi, input[inputi] == find[0], output_size, inputLen, &(input[inputi])); + if(input[inputi] == find[0] && strncmp(&(input[inputi]), find, findLen) == 0){ + memcpy(&(output_str[desti]), replace, replaceLen); + // printf("replaced: %s\n", output_str); + desti += replaceLen; + inputi += findLen; + } else { + // printf("outputstr: %s\n", output_str); + output_str[desti++] = input[inputi++]; + } + } + + output_str[output_size - 1] = '\0'; + + return output_size; + +} + + char * sc_str_quote(const char *src) { size_t len = strlen(src); diff --git a/app/src/util/str.h b/app/src/util/str.h index 1736bd95..6d79ae8e 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -26,6 +26,27 @@ sc_strncpy(char *dest, const char *src, size_t n); size_t sc_str_join(char *dst, const char *const tokens[], char sep, size_t n); +/** + * String find and replace all occurrences. + * Compatible with different sizes of find and the replacement + * Only creates output string if changes exist + * WARNING: You are responsible for freeing the output if it's not NULL + * + * @param input The string to find on and replace with matches found + * @param find What to find in the input + * @param replace What to replace with for each found instance from find + * @param output Null or a pointer to the char* which contains the replaced char* + * @return int64_t + * if > 0: The size of the string in output + * if < 1: output should be ignored and: + * if == 0: Nothing changed from the original string + * if == -2: Overflow when trying to create the replaced string or OOM + * + * + */ +int64_t +sc_str_find_replace(char* input, char* find, char* replace, char** output); + /** * Quote a string * From d5156dfb4fa18e38eeddff407c1d44736d8ae949 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 16 Apr 2022 14:46:52 +0100 Subject: [PATCH 3/5] Add hookScript ability to the server --- .../java/com/genymobile/scrcpy/CleanUp.java | 25 ++++++++++++++++--- .../java/com/genymobile/scrcpy/Options.java | 9 +++++++ .../java/com/genymobile/scrcpy/Server.java | 21 +++++++++++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 319a957d..8d402240 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -33,9 +33,9 @@ public final class CleanUp { } }; - private static final int FLAG_DISABLE_SHOW_TOUCHES = 1; - private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2; - private static final int FLAG_POWER_OFF_SCREEN = 4; + private static final int FLAG_DISABLE_SHOW_TOUCHES = 1 << 0; + private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 1 << 1; + private static final int FLAG_POWER_OFF_SCREEN = 1 << 2; private int displayId; @@ -46,6 +46,7 @@ public final class CleanUp { private boolean disableShowTouches; private boolean restoreNormalPowerMode; private boolean powerOffScreen; + public String hookScript; public Config() { // Default constructor, the fields are initialized by CleanUp.configure() @@ -58,6 +59,7 @@ public final class CleanUp { disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0; restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0; powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0; + hookScript = in.readString(); } @Override @@ -75,6 +77,7 @@ public final class CleanUp { options |= FLAG_POWER_OFF_SCREEN; } dest.writeByte(options); + dest.writeString(hookScript); } private boolean hasWork() { @@ -116,7 +119,10 @@ public final class CleanUp { // not instantiable } - public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen) + public static void configure( + int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen, + String hookScript + ) throws IOException { Config config = new Config(); config.displayId = displayId; @@ -124,6 +130,7 @@ public final class CleanUp { config.restoreStayOn = restoreStayOn; config.restoreNormalPowerMode = restoreNormalPowerMode; config.powerOffScreen = powerOffScreen; + config.hookScript = hookScript == null ? "" : hookScript; if (config.hasWork()) { startProcess(config); @@ -193,5 +200,15 @@ public final class CleanUp { Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); } } + + if (!config.hookScript.isEmpty()) { + try { + Command.execShellScript(config.hookScript, "stop"); + } catch (IOException e) { + Ln.e("Something failed while trying to run the stop hook", e); + } catch (InterruptedException e) { + Ln.e("Got interrupted while running the start hook", e); + } + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 4842b635..a3417c42 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -18,6 +18,7 @@ public class Options { private boolean stayAwake; private List codecOptions; private String encoderName; + private String hookScript; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; private boolean downsizeOnError = true; @@ -132,6 +133,14 @@ public class Options { this.encoderName = encoderName; } + public void setHookScript(String hookScript) { + this.hookScript = hookScript; + } + + public String getHookScript() { + return this.hookScript; + } + public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { this.powerOffScreenOnClose = powerOffScreenOnClose; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 60f485d8..7dde93b7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -6,6 +6,7 @@ import android.os.BatteryManager; import android.os.Build; import java.io.IOException; +import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -53,11 +54,22 @@ public final class Server { if (options.getCleanup()) { try { CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode, - options.getPowerOffScreenOnClose()); + options.getPowerOffScreenOnClose(), options.getHookScript()); } catch (IOException e) { Ln.e("Could not configure cleanup", e); } } + + try { + String hookScript = options.getHookScript(); + if(hookScript != null && !hookScript.isEmpty()){ + Command.execShellScript(hookScript, "start", "--pid", String.valueOf(android.os.Process.myPid())); + } + } catch (IOException e) { + Ln.e("Something failed while trying to run the start hook", e); + } catch (InterruptedException e) { + Ln.e("Got interrupted while running the start hook", e); + } } private static void scrcpy(Options options) throws IOException { @@ -170,6 +182,8 @@ public final class Server { Options options = new Options(); + Ln.e("Args are these: " + Arrays.toString(args)); + for (int i = 1; i < args.length; ++i) { String arg = args[i]; int equalIndex = arg.indexOf('='); @@ -232,6 +246,11 @@ public final class Server { options.setEncoderName(value); } break; + case "hook_script": + if (!value.isEmpty()) { + options.setHookScript(value); + } + break; case "power_off_on_close": boolean powerOffScreenOnClose = Boolean.parseBoolean(value); options.setPowerOffScreenOnClose(powerOffScreenOnClose); From 2ea9cdd0227cad6dd5c59af1026afd56d6e248ff Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 16 Apr 2022 14:47:57 +0100 Subject: [PATCH 4/5] Add hookScript options to scrcpy --- app/src/cli.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++ app/src/options.h | 1 + app/src/scrcpy.c | 1 + 3 files changed, 61 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index 5dda86e5..703b519b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -56,6 +56,7 @@ #define OPT_OTG 1036 #define OPT_NO_CLEANUP 1037 #define OPT_PRINT_FPS 1038 +#define OPT_HOOK_SCRIPT 1039 struct sc_option { char shortopt; @@ -206,6 +207,18 @@ static const struct sc_option options[] = { .longopt = "help", .text = "Print this help.", }, + { + .longopt_id = OPT_HOOK_SCRIPT, + .longopt = "hook-script", + .argdesc = "path", + .text = "The path to a linux shell script which is run on your device " + "when a meaningful event happens.\n" + "The script is run with multiple parameters. The ones actually " + "used will depend on the event and intended functionality.\n" + "In this version, only the events ('START', 'STOP') have been " + "implemented. Others may be implemented in the future.\n" + "For details on the parameters, check the README manual", + }, { .longopt_id = OPT_LEGACY_PASTE, .longopt = "legacy-paste", @@ -1326,6 +1339,47 @@ sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { } #endif + +static bool +parse_hook_script(const char *hook_script_path, char **hook_script) { + const int MAX_ACCEPTABLE_SIZE = 1048576; // 1MB + if(!hook_script_path) { + return false; + } + + FILE *script_file = fopen(hook_script_path, "rb"); + if(script_file == NULL){ + perror("Cannot open script file\n"); + return false; + } + fseek(script_file, 0, SEEK_END); + long ssize = ftell(script_file); + fseek(script_file, 0, SEEK_SET); + + if(ssize > MAX_ACCEPTABLE_SIZE){ + LOGE("Script file too large. " + "Only up to 1MB (%d bytes) is accepted\n", MAX_ACCEPTABLE_SIZE); + return false; + } + + + *hook_script = malloc(ssize + 1); + if(*hook_script == NULL){ + LOG_OOM(); + return false; + } + if(!fread(*hook_script, ssize, 1, script_file)){ + perror("Cannot read script file"); + return false; + } + fclose(script_file); + + (*hook_script)[ssize] = '\0'; + + + return true; +} + static bool parse_record_format(const char *optarg, enum sc_record_format *format) { if (!strcmp(optarg, "mp4")) { @@ -1574,6 +1628,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_FORWARD_ALL_CLICKS: opts->forward_all_clicks = true; break; + case OPT_HOOK_SCRIPT: + if (!parse_hook_script(optarg, &opts->hook_script)) { + return false; + } + break; case OPT_LEGACY_PASTE: opts->legacy_paste = true; break; diff --git a/app/src/options.h b/app/src/options.h index f63e5c42..ca88d0d7 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -129,6 +129,7 @@ struct scrcpy_options { bool disable_screensaver; bool forward_key_repeat; bool forward_all_clicks; + char * hook_script; bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8fbfe394..73fd1892 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -318,6 +318,7 @@ scrcpy(struct scrcpy_options *options) { .codec_options = options->codec_options, .encoder_name = options->encoder_name, .force_adb_forward = options->force_adb_forward, + .hook_script = options->hook_script, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, .downsize_on_error = options->downsize_on_error, From 4287759e06a932f7c7e944987768baf65e3d8239 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 16 Apr 2022 14:50:59 +0100 Subject: [PATCH 5/5] Run server with escaped hook_script argument Unlike what would be expected, adb shell just concatenates the arguments as strings same as if they were just separated by spaces. Sending the commands in one argument or sending as multiple arguments doesn't preduce any different outcome. Sad but that's how adb works. try: adb shell 'am broadcast' vs adb shell am broadcast If they both work, means there's no reasonable way to deliver the arguments as-is. They have to be escaped. --- app/src/server.c | 18 ++++++++++++++++++ app/src/server.h | 1 + 2 files changed, 19 insertions(+) diff --git a/app/src/server.c b/app/src/server.c index c02390a4..712a6f4a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -93,6 +93,7 @@ sc_server_params_copy(struct sc_server_params *dst, COPY(crop); COPY(codec_options); COPY(encoder_name); + COPY(hook_script); COPY(tcpip_dst); #undef COPY @@ -233,6 +234,23 @@ execute_server(struct sc_server *server, if (params->encoder_name) { ADD_PARAM("encoder_name=%s", params->encoder_name); } + if (params->hook_script) { + char* requoted_hook; + int64_t replace_result = sc_str_find_replace(params->hook_script, "'", "'\"'\"'", &requoted_hook); + switch(replace_result){ + case -2: + LOG_OOM(); + break; + case -1: + case 0: + ADD_PARAM("hook_script='%s'", params->hook_script); + break; + default: + ADD_PARAM("hook_script='%s'", requoted_hook); + free(requoted_hook); + } + + } if (params->power_off_on_close) { ADD_PARAM("power_off_on_close=true"); } diff --git a/app/src/server.h b/app/src/server.h index 5f630ca8..cf52e963 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -39,6 +39,7 @@ struct sc_server_params { bool show_touches; bool stay_awake; bool force_adb_forward; + const char * hook_script; bool power_off_on_close; bool clipboard_autosync; bool downsize_on_error;