diff --git a/build.gradle b/build.gradle index e17b518..85ad8a9 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ plugins { id 'com.android.library' } -def buildApi = rootProject.ext.buildApi +def buildApi = project.hasProperty("specialApi") ? specialApi: rootProject.ext.buildApi def linkerModuleName = 'fake-linker' def hookInstallModuleName = 'hook-install' def mVersionCode = 1000 @@ -39,8 +39,8 @@ android { externalNativeBuild { cmake { - cppFlags "" - cFlags "" + cppFlags "-Werror" + cFlags "-Werror" abiFilters 'armeabi-v7a', 'x86', 'arm64-v8a', 'x86_64' arguments "-DANDROID_PLATFORM=${buildApi}", "-DLINKER_MODULE_NAME=${linkerModuleName}", "-DHOOK_INSTALL_MODULE_NAME=${hookInstallModuleName}", @@ -57,8 +57,15 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + + externalNativeBuild { + cmake { + cppFlags "-fvisibility=hidden -fvisibility-inlines-hidden -Os -fomit-frame-pointer -fno-rtti -fno-exceptions" + cFlags "-fvisibility=hidden -fvisibility-inlines-hidden -Os -fomit-frame-pointer" + } + } } } compileOptions { @@ -88,7 +95,7 @@ tasks.whenTaskAdded { task -> it.outputs.files.files.each { File dir -> dir.eachFileRecurse { if (it.name == "lib${linkerModuleName}-${buildApi}.so") { - println '找到当前正在编译的fake-linker模块版本,需要删除' + println 'delete current fake-linker version module' it.delete() } } @@ -96,12 +103,6 @@ tasks.whenTaskAdded { task -> } } } -dependencies { - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.2.1' - testImplementation 'junit:junit:4.13.1' - implementation 'eu.chainfire:libsuperuser:1.0.0.201811281328' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' +dependencies { } \ No newline at end of file diff --git a/proguard-rules.pro b/proguard-rules.pro index 481bb43..48ba95d 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -18,4 +18,31 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-keep public class com.sanfengandroid.fakelinker.FakeLinker { + *; +} + +-keep public class com.sanfengandroid.fakelinker.FileInstaller { + *; +} + +-keep public class com.sanfengandroid.fakelinker.ErrorCode { + *; +} + +-keep class eu.chainfire.libsuperuser.Shell { + public *; +} + +-keep class eu.chainfire.libsuperuser.Debug { + public *; +} +-keep class eu.chainfire.libsuperuser.ShellNotClosedException { + *; +} + +-keep class eu.chainfire.libsuperuser.ShellOnMainThreadException{ + *; +} \ No newline at end of file diff --git a/src/main/assets/arm64-v8a/hook-install b/src/main/assets/arm64-v8a/hook-install index 142f718..0414c58 100644 Binary files a/src/main/assets/arm64-v8a/hook-install and b/src/main/assets/arm64-v8a/hook-install differ diff --git a/src/main/assets/armeabi-v7a/hook-install b/src/main/assets/armeabi-v7a/hook-install index 9d52c3c..bd078e3 100644 Binary files a/src/main/assets/armeabi-v7a/hook-install and b/src/main/assets/armeabi-v7a/hook-install differ diff --git a/src/main/assets/x86/hook-install b/src/main/assets/x86/hook-install index 515a184..0a78523 100644 Binary files a/src/main/assets/x86/hook-install and b/src/main/assets/x86/hook-install differ diff --git a/src/main/assets/x86_64/hook-install b/src/main/assets/x86_64/hook-install index 2509e95..6b8f158 100644 Binary files a/src/main/assets/x86_64/hook-install and b/src/main/assets/x86_64/hook-install differ diff --git a/src/main/cpp/CMakeLists.txt b/src/main/cpp/CMakeLists.txt index e6f9b49..614103f 100644 --- a/src/main/cpp/CMakeLists.txt +++ b/src/main/cpp/CMakeLists.txt @@ -11,11 +11,6 @@ cmake_minimum_required(VERSION 3.4.1) # Gradle automatically packages shared libraries with your APK. string(TOLOWER ${CMAKE_BUILD_TYPE} THIS_BUILD_TYPE) -set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -Os -Oz -ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden -Werror") -set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os -Oz -fno-rtti -fno-exceptions -ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden -Werror") -set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Werror") -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror") - if (${CMAKE_ANDROID_ARCH_ABI} STREQUAL "arm64-v8a" OR ${CMAKE_ANDROID_ARCH_ABI} STREQUAL "x86_64") add_definitions(-DUSE_RELA) else () @@ -32,7 +27,7 @@ include_directories(common) include_directories(export) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../libs/${THIS_BUILD_TYPE}/${CMAKE_ANDROID_ARCH_ABI}) -message(WARNING "当前版本:${ANDROID_PLATFORM},输出目录:${CMAKE_LIBRARY_OUTPUT_DIRECTORY}, 编译类型:${THIS_BUILD_TYPE},模块名:${LINKER_MODULE_NAME},安装模块${HOOK_INSTALL_MODULE_NAME}") +message(STATUS "current version:${ANDROID_PLATFORM}, output dir:${CMAKE_LIBRARY_OUTPUT_DIRECTORY}, build type:${THIS_BUILD_TYPE}, linker module name:${LINKER_MODULE_NAME}, install module name:${HOOK_INSTALL_MODULE_NAME}") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/module_config.h.in" "${CMAKE_CURRENT_SOURCE_DIR}/common/module_config.h") add_library( # Sets the name of the library. @@ -97,6 +92,10 @@ set_target_properties(${LINKER_MODULE_NAME}-${ANDROID_PLATFORM} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../libs/${THIS_BUILD_TYPE}/${CMAKE_ANDROID_ARCH_ABI}) +set_target_properties(${LINKER_MODULE_NAME}-${ANDROID_PLATFORM} PROPERTIES + LINK_FLAGS_RELEASE + "${LINK_FLAGS_RELEASE} -Wl,--gc-sections,-S,--version-script=\"${CMAKE_CURRENT_SOURCE_DIR}/symbol.map.txt\"") + target_link_libraries( ${HOOK_INSTALL_MODULE_NAME} ${log-lib}) diff --git a/src/main/cpp/export/linker_export.h b/src/main/cpp/export/linker_export.h index 90e9f81..96f9dfa 100644 --- a/src/main/cpp/export/linker_export.h +++ b/src/main/cpp/export/linker_export.h @@ -144,7 +144,7 @@ enum SoinfoFunType { kSFCallDlopen }; -//API_PUBLIC void *call_soinfo_function(SoinfoFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code); +//C_API API_PUBLIC void *call_soinfo_function(SoinfoFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code); enum CommonFunType { // 添加一个soinfo到全局库 @@ -164,7 +164,7 @@ enum CommonFunType { kCFCallManualRelinks, }; -//API_PUBLIC void *call_common_function(CommonFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code); +//C_API API_PUBLIC void *call_common_function(CommonFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code); #if __ANDROID_API__ >= __ANDROID_API_N__ @@ -231,7 +231,7 @@ enum NamespaceFunType { * 调用相关命名空间函数 * @return 若返回值是多个集合,则都包装成VarLengthObject对象 * */ -//API_PUBLIC void * +//C_API API_PUBLIC void * //call_namespace_function(NamespaceFunType fun_type, NamespaceParamType find_type, const void *find_param, NamespaceParamType param_type, const void *param, int *error_code); #endif diff --git a/src/main/cpp/linker/fake_linker.cpp b/src/main/cpp/linker/fake_linker.cpp index 2be3985..45a14e3 100644 --- a/src/main/cpp/linker/fake_linker.cpp +++ b/src/main/cpp/linker/fake_linker.cpp @@ -372,7 +372,7 @@ static int ResolveInnerSymbolsAddress(const char *library_name, symbols_address for (int i = 0; i < ret->len; ++i) { if (ret->elements[i] != 0) { ret->elements[i] += base; - LOGD("found symbol: %s, library: %s, address: %" PRIx64, symbols[i], library_name, ret->elements[i]); + LOGV("found symbol: %s, library: %s, address: %" PRIx64, symbols[i], library_name, ret->elements[i]); } } return 0; diff --git a/src/main/cpp/linker/linker_common_types.cpp b/src/main/cpp/linker/linker_common_types.cpp index 7fb9e29..7cfb720 100644 --- a/src/main/cpp/linker/linker_common_types.cpp +++ b/src/main/cpp/linker/linker_common_types.cpp @@ -31,7 +31,7 @@ static void Init() { "__dl__ZL21g_namespace_allocator", "__dl__ZL26g_namespace_list_allocator")); if (symbols.data == nullptr) { - LOGD("find symbol g_soinfo_allocator/g_soinfo_links_allocator/g_namespace_allocator/g_namespace_list_allocator failed"); + LOGE("find symbol g_soinfo_allocator/g_soinfo_links_allocator/g_namespace_allocator/g_namespace_list_allocator failed"); return; } g_soinfo_allocator = static_cast *>(GSIZE_TO_POINTER(symbols.data->elements[0])); diff --git a/src/main/cpp/linker/linker_export.cpp b/src/main/cpp/linker/linker_export.cpp index ef99761..88948ba 100644 --- a/src/main/cpp/linker/linker_export.cpp +++ b/src/main/cpp/linker/linker_export.cpp @@ -61,7 +61,7 @@ static soinfo *find_soinfo_by_type(SoinfoParamType type, const void *p, void *ca return result; } -API_PUBLIC void *call_soinfo_function(SoinfoFunType fun_type, SoinfoParamType find_type, const void *find_param, +C_API API_PUBLIC void *call_soinfo_function(SoinfoFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code) { void *result = nullptr; soinfo *so; @@ -178,7 +178,7 @@ API_PUBLIC void *call_soinfo_function(SoinfoFunType fun_type, SoinfoParamType fi return result; } -API_PUBLIC void *call_common_function(CommonFunType fun_type, SoinfoParamType find_type, const void *find_param, +C_API API_PUBLIC void *call_common_function(CommonFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code) { void *result = nullptr; soinfo *so; @@ -362,7 +362,7 @@ static soinfo *find_soinfo(NamespaceParamType type, const void *param, void *cal return result; } -API_PUBLIC void *call_namespace_function(NamespaceFunType fun_type, NamespaceParamType find_type, const void +C_API API_PUBLIC void *call_namespace_function(NamespaceFunType fun_type, NamespaceParamType find_type, const void *find_param, NamespaceParamType param_type, const void *param, int *error_code) { void *result = nullptr; android_namespace_t *np = nullptr; diff --git a/src/main/cpp/linker/linker_globals.cpp b/src/main/cpp/linker/linker_globals.cpp index 51070ef..4a63d69 100644 --- a/src/main/cpp/linker/linker_globals.cpp +++ b/src/main/cpp/linker/linker_globals.cpp @@ -434,7 +434,7 @@ void *ProxyLinker::FindSymbolByDlsym(soinfo *si, const char *name) { #else result = CallDoDlsym(si, name); #endif - LOGD("dlsym soinfo name: %s, symbol: %s, address: %p", si->get_soname() == nullptr ? "(null)" : si->get_soname(), name, result); + LOGV("dlsym soinfo name: %s, symbol: %s, address: %p", si->get_soname() == nullptr ? "(null)" : si->get_soname(), name, result); return result; } @@ -724,6 +724,7 @@ bool ProxyLinker::ManualRelinkLibraries(soinfo *global, const VarLengthObjectget_soname() == nullptr ? "(null)" : global->get_soname()); + return false; } bool success = true; for (int i = 0; i < vars->len; ++i) { @@ -766,7 +767,9 @@ bool ProxyLinker::ManualRelinkLibrary(symbol_relocations *rels, soinfo *child) { return child->again_process_relocation(rels); } - +/* + * 调用系统重定位会出现各种问题,废弃使用 + * */ bool ProxyLinker::SystemRelinkLibrary(soinfo *so) { bool success; if (so == nullptr) { @@ -780,7 +783,6 @@ bool ProxyLinker::SystemRelinkLibrary(soinfo *so) { linker_block_protect_all(PROT_READ | PROT_WRITE); so->set_unlinked(); std::string str = soinfo_to_string(so); - LOGD("system relink soinfo: %s", str.c_str()); // 重新dlopen出错,因为目前进程已经存在该so就不会在走ElfRead // 5.0查找会修改linker的数据段,因此还要解保护linker MapsUtil util(so->get_realpath()); @@ -797,7 +799,7 @@ bool ProxyLinker::SystemRelinkLibrary(soinfo *so) { so->set_linked(); } util.RecoveryPageProtect(); - LOGD("The system relink library: %s, result: %s", so->get_soname(), success ? "true" : "false"); + LOGV("The system relink library: %s, result: %s", so->get_soname(), success ? "true" : "false"); linker_block_protect_all(PROT_READ); return success; } @@ -868,14 +870,14 @@ void ProxyLinker::Init() { CHECK(g_default_namespace_ptr); CHECK(g_soinfo_handles_map_ptr); - LOGD("find linker soinfo: %p, g_ld_debug_verbosity:%p, g_default_namespace: %p, g_soinfo_handles_map: %p, link_image: %p", + LOGV("find linker soinfo: %p, g_ld_debug_verbosity:%p, g_default_namespace: %p, g_soinfo_handles_map: %p, link_image: %p", solist_ptr, g_ld_debug_verbosity_ptr, g_default_namespace_ptr, g_soinfo_handles_map_ptr, link_image_ptr); #elif __ANDROID_API__ == __ANDROID_API_M__ #else g_ld_preloads_ptr = (soinfo **) symbols.data->elements[5]; CHECK(g_ld_preloads_ptr); - LOGD("find soinfo: %p, g_ld_preloads: %p, link_image: %p", solist_ptr, g_ld_preloads_ptr, link_image_ptr); + LOGV("find soinfo: %p, g_ld_preloads: %p, link_image: %p", solist_ptr, g_ld_preloads_ptr, link_image_ptr); #endif #if __ANDROID_API__ >= __ANDROID_API_O__ @@ -886,7 +888,7 @@ void ProxyLinker::Init() { dlsym_ptr = reinterpret_cast(linker_soinfo_ptr->find_export_symbol_address("__loader_dlsym")); create_namespace_ptr = reinterpret_cast (linker_soinfo_ptr->find_export_symbol_address("__loader_android_create_namespace")); - LOGD("linker __loader_android_dlopen_ext: %p, __loader_dlsym: %p, __loader_android_create_namespace: %p", dlopen_ptr, dlsym_ptr, create_namespace_ptr); + LOGV("linker __loader_android_dlopen_ext: %p, __loader_dlsym: %p, __loader_android_create_namespace: %p", dlopen_ptr, dlsym_ptr, create_namespace_ptr); CHECK(create_namespace_ptr); #else linker_soinfo_ptr = ProxyLinker::FindSoinfoByName("libdl.so"); diff --git a/src/main/cpp/linker/linker_soinfo.cpp b/src/main/cpp/linker/linker_soinfo.cpp index 80f0aeb..71a9dea 100644 --- a/src/main/cpp/linker/linker_soinfo.cpp +++ b/src/main/cpp/linker/linker_soinfo.cpp @@ -601,7 +601,7 @@ static bool process_relocation(soinfo *so, const rel_t &reloc, symbol_relocation if (Mode == RelocMode::JumpTable) { if (r_type == R_GENERIC_JUMP_SLOT) { *static_cast(rel_target) = sym_addr + get_addend_norel(); - LOGD("Relocation symbol JumpTable: %s, original address: %p, new address: %p", sym_name, reinterpret_cast(orig), + LOGV("Relocation symbol JumpTable: %s, original address: %p, new address: %p", sym_name, reinterpret_cast(orig), reinterpret_cast(*static_cast(rel_target))); return true; } @@ -609,12 +609,12 @@ static bool process_relocation(soinfo *so, const rel_t &reloc, symbol_relocation if (Mode == RelocMode::Typical) { if (r_type == R_GENERIC_ABSOLUTE) { *static_cast(rel_target) = sym_addr + get_addend_rel(); - LOGD("Relocation symbol Typical ABSOLUTE: %s, original address: %16p, new address: %16p", sym_name, reinterpret_cast(orig), + LOGV("Relocation symbol Typical ABSOLUTE: %s, original address: %16p, new address: %16p", sym_name, reinterpret_cast(orig), reinterpret_cast(*static_cast(rel_target))); return true; } else if (r_type == R_GENERIC_GLOB_DAT) { *static_cast(rel_target) = sym_addr + get_addend_norel(); - LOGD("Relocation symbol Typical GLOB_DAT: %s, original address: %16p, new address: %16p", sym_name, reinterpret_cast(orig), + LOGV("Relocation symbol Typical GLOB_DAT: %s, original address: %16p, new address: %16p", sym_name, reinterpret_cast(orig), reinterpret_cast(*static_cast(rel_target))); return true; } @@ -739,7 +739,7 @@ bool soinfo::again_process_relocation(symbol_relocations *rels) { LOGE("cannot change soinfo: %s memory protect", get_soname() == nullptr ? "(null)" : get_soname()); return false; } - LOGD("again relocation library: %s", get_soname()); + LOGV("again relocation library: %s", get_soname()); #if defined(USE_RELA) if (rela_ != nullptr) { plain_relocate_impl(this, rela_, rela_count_, rels); diff --git a/src/main/cpp/linker/linker_util.cpp b/src/main/cpp/linker/linker_util.cpp index a48598a..7c890d5 100644 --- a/src/main/cpp/linker/linker_util.cpp +++ b/src/main/cpp/linker/linker_util.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include @@ -17,174 +17,192 @@ MAYBE_MAP_FLAG((x), PF_R, PROT_READ) | \ MAYBE_MAP_FLAG((x), PF_W, PROT_WRITE)) +#define MAX_OUT 8192 + +#define STRCAT_MES(msg) \ + do{ \ + if (msg != nullptr) \ + strncat(out, msg, MAX_OUT); \ + else \ + strncat(out, "null", MAX_OUT);\ + }while (0) + +#define STRCAT_BOOLEAN(msg, cond) STRCAT_MES(msg); strncat(out, cond ? "true" : "false", MAX_OUT) +#define STRCAT_STRING(msg, str) STRCAT_MES(msg); STRCAT_MES(str); +#define STRCAT_HEX(msg, value) \ + do { \ + STRCAT_MES(msg); \ + size_t len_ = strlen(out);\ + snprintf(&out[len_], (size_t)(MAX_OUT - len_), "0x%" SCNx64, (uint64_t)value);\ + } while (0) std::string android_namespace_to_string(android_namespace_t *np) { - if (np == nullptr){ - return "android_namespace_t is null."; - } - std::stringstream sstream; - size_t len; + if (np == nullptr) { + return "android_namespace_t is null."; + } + char out[MAX_OUT]; + out[0] = '\0'; + size_t len; + + STRCAT_STRING("namespace name: ", np->get_name()); + STRCAT_BOOLEAN(", isolated: ", np->is_isolated_); - sstream << "namespace name: " << np->get_name() << ", isolated: " << std::boolalpha << np->is_isolated_; #if __ANDROID_API__ >= __ANDROID_API_O__ - sstream << ", greylist enabled: " << std::boolalpha << np->is_greylist_enabled_; + STRCAT_BOOLEAN(", greylist enabled: ", np->is_greylist_enabled_); #endif #if __ANDROID_API__ >= __ANDROID_API_R__ - sstream << ", use anonymous: " << std::boolalpha << np->is_also_used_as_anonymous_; + STRCAT_BOOLEAN(", use anonymous: ", np->is_also_used_as_anonymous_); #endif - auto format = [&sstream](const std::vector &input, const char *name) { - sstream << name << "{"; - size_t len = input.size(); - for (size_t i = 0; i < len; i++) { - sstream << input[i]; - if (i != len - 1) { - sstream << ", "; - } - } - sstream << "}"; - }; - sstream << ", \n"; - format(np->ld_library_paths_, "ld_library"); - sstream << ", \n"; - format(np->default_library_paths_, "default_library_paths"); - sstream << ", \n"; - format(np->permitted_paths_, "permitted_paths"); + auto format = [&](const std::vector &input, const char *name) { + STRCAT_STRING(name, "{"); + size_t len = input.size(); + for (size_t i = 0; i < len; i++) { + STRCAT_MES(input[i].c_str()); + if (i != len - 1) { + STRCAT_MES(", "); + } + } + STRCAT_MES("}"); + }; + STRCAT_MES(", \n"); + format(np->ld_library_paths_, "ld_library"); + STRCAT_MES(", \n"); + format(np->default_library_paths_, "default_library_paths"); + STRCAT_MES(", \n"); + format(np->permitted_paths_, "permitted_paths"); #if __ANDROID_API__ >= __ANDROID_API_Q__ - sstream << ", \n"; - format(np->whitelisted_libs_, "whitelisted_libs"); + STRCAT_MES(", \n"); + format(np->whitelisted_libs_, "whitelisted_libs"); #endif #if __ANDROID_API__ >= __ANDROID_API_O__ - sstream << ", \n"; - sstream << "linked_namespaces{"; - len = np->linked_namespaces_.size(); - for (size_t i = 0; i < len; i++) { - sstream << np->linked_namespaces_[i].linked_namespace_->get_name(); - if (i != len - 1) { - sstream << ", "; - } - } - sstream << "}"; + STRCAT_MES(", \nlinked_namespaces{"); + len = np->linked_namespaces_.size(); + for (size_t i = 0; i < len; i++) { + STRCAT_MES(np->linked_namespaces_[i].linked_namespace_->get_name()); + if (i != len - 1) { + STRCAT_MES(", "); + } + } + STRCAT_MES("}"); #endif - sstream << ",solist{\n"; - for (auto si : np->soinfo_list_) { - sstream << si->get_realpath() << "\n"; - } - sstream << "}"; - sstream.flush(); - return sstream.str(); + STRCAT_MES(",solist{\n"); + for (auto si : np->soinfo_list_) { + STRCAT_STRING(si->get_realpath(), "\n"); + } + STRCAT_MES("}"); + return out; } std::string soinfo_to_string(soinfo *si) { - if (si == nullptr) { - return "soinfo is null."; - } - std::stringstream sstream; - - sstream << "soinfo name: " << (si->get_soname() == nullptr ? "null" : si->get_soname()); - sstream << ", base address: " << std::hex << si->base; - sstream << ", flags: " << std::hex << si->flags_ << ", load bias: " << std::hex << si->load_bias; - - auto format = [&sstream](soinfo_list_t &list, const char *name) { - sstream << name << "{"; - size_t size = list.size(); - list.for_each([&](soinfo *so) { - sstream << (so->get_soname() == nullptr ? "(null)" : so->get_soname()); - if (--size != 0) { - sstream << ", "; - } - }); - sstream << "}"; - }; - sstream << ", "; - format(si->children_, "children"); - sstream << ", "; - format(si->parents_, "parents"); + if (si == nullptr) { + return "soinfo is null."; + } + char out[MAX_OUT]; + out[0] = '\0'; + + STRCAT_STRING("soinfo name: ", si->get_soname()); + STRCAT_HEX(", base address: ", si->base); + STRCAT_HEX(", flags: ", si->flags_); + STRCAT_HEX(", load bias: ", si->load_bias); + + auto format = [&](soinfo_list_t &list, const char *name) { + STRCAT_STRING(name, "{"); + size_t size = list.size(); + list.for_each([&](soinfo *so) { + STRCAT_MES(so->get_soname()); + if (--size != 0) { + STRCAT_MES(", "); + } + }); + STRCAT_MES("}"); + }; + STRCAT_MES(", "); + format(si->children_, "children"); + STRCAT_MES(", "); + format(si->parents_, "parents"); #if __ANDROID_API__ >= __ANDROID_API_L_MR1__ - sstream << ", rtld_flags: " << std::hex << si->rtld_flags_; + STRCAT_HEX(", rtld_flags: ", si->rtld_flags_); #endif #if __ANDROID_API__ >= __ANDROID_API_M__ // 6.0以上 - sstream << ", dt_flags_1: " << std::hex << si->dt_flags_1_; - sstream << ", realpath: " << si->realpath_; + STRCAT_HEX(", dt_flags_1: ", si->dt_flags_1_); + STRCAT_STRING(", realpath: ", si->realpath_.c_str()); #endif #if __ANDROID_API__ >= __ANDROID_API_N__ - size_t len = si->dt_runpath_.size(); - sstream << ", dt_runpath:{"; - for (int i = 0; i < len; ++i) { - sstream << si->dt_runpath_[i]; - if (i != len - 1) { - sstream << ", "; - } - } - sstream << "}"; - sstream << ", primary namespace: " << si->primary_namespace_->get_name(); - sstream << ", secondary_namespaces{"; - len = si->secondary_namespaces_.size(); - si->get_secondary_namespaces().for_each([&](android_namespace_t *np) { - sstream << np->get_name(); - if (--len != 0) { - sstream << ", "; - } - }); - sstream << "}"; - sstream << ", handle: " << std::hex << si->get_handle(); + size_t len = si->dt_runpath_.size(); + STRCAT_MES(", dt_runpath:{"); + for (int i = 0; i < len; ++i) { + STRCAT_MES(si->dt_runpath_[i].c_str()); + if (i != len - 1) { + STRCAT_MES(", "); + } + } + STRCAT_STRING("}, primary namespace: " ,si->primary_namespace_->get_name()); + STRCAT_MES(", secondary_namespaces{"); + len = si->secondary_namespaces_.size(); + si->get_secondary_namespaces().for_each([&](android_namespace_t *np) { + STRCAT_MES(np->get_name()); + if (--len != 0) { + STRCAT_MES(", "); + } + }); + STRCAT_HEX("}, handle: ", si->get_handle()); #endif - sstream.flush(); - return sstream.str(); + return out; } bool file_is_in_dir(const std::string &file, const std::string &dir) { - const char *needle = dir.c_str(); - const char *haystack = file.c_str(); - size_t needle_len = strlen(needle); + const char *needle = dir.c_str(); + const char *haystack = file.c_str(); + size_t needle_len = strlen(needle); - return strncmp(haystack, needle, needle_len) == 0 && - haystack[needle_len] == '/' && - strchr(haystack + needle_len + 1, '/') == nullptr; + return strncmp(haystack, needle, needle_len) == 0 && + haystack[needle_len] == '/' && + strchr(haystack + needle_len + 1, '/') == nullptr; } bool file_is_under_dir(const std::string &file, const std::string &dir) { - const char *needle = dir.c_str(); - const char *haystack = file.c_str(); - size_t needle_len = strlen(needle); + const char *needle = dir.c_str(); + const char *haystack = file.c_str(); + size_t needle_len = strlen(needle); - return strncmp(haystack, needle, needle_len) == 0 && - haystack[needle_len] == '/'; + return strncmp(haystack, needle, needle_len) == 0 && + haystack[needle_len] == '/'; } static int _phdr_table_set_load_prot(const ElfW(Phdr) *phdr_table, size_t phdr_count, - ElfW(Addr) load_bias, int extra_prot_flags) { - const ElfW(Phdr) *phdr = phdr_table; - const ElfW(Phdr) *phdr_limit = phdr + phdr_count; - - for (; phdr < phdr_limit; phdr++) { - if (phdr->p_type != PT_LOAD || (phdr->p_flags & PF_W) != 0) { - continue; - } - - ElfW(Addr) seg_page_start = PAGE_START(phdr->p_vaddr) + load_bias; - ElfW(Addr) seg_page_end = PAGE_END(phdr->p_vaddr + phdr->p_memsz) + load_bias; - - int prot = PFLAGS_TO_PROT(phdr->p_flags); - if ((extra_prot_flags & PROT_WRITE) != 0) { - // make sure we're never simultaneously writable / executable - prot &= ~PROT_EXEC; - } - - int ret = mprotect(reinterpret_cast(seg_page_start), - seg_page_end - seg_page_start, - prot | extra_prot_flags); - if (ret < 0) { - return -1; - } - } - return 0; + ElfW(Addr) load_bias, int extra_prot_flags) { + const ElfW(Phdr) *phdr = phdr_table; + const ElfW(Phdr) *phdr_limit = phdr + phdr_count; + + for (; phdr < phdr_limit; phdr++) { + if (phdr->p_type != PT_LOAD || (phdr->p_flags & PF_W) != 0) { + continue; + } + + ElfW(Addr) seg_page_start = PAGE_START(phdr->p_vaddr) + load_bias; + ElfW(Addr) seg_page_end = PAGE_END(phdr->p_vaddr + phdr->p_memsz) + load_bias; + + int prot = PFLAGS_TO_PROT(phdr->p_flags); + if ((extra_prot_flags & PROT_WRITE) != 0) { + // make sure we're never simultaneously writable / executable + prot &= ~PROT_EXEC; + } + + int ret = mprotect(reinterpret_cast(seg_page_start), + seg_page_end - seg_page_start, + prot | extra_prot_flags); + if (ret < 0) { + return -1; + } + } + return 0; } int phdr_table_protect_segments(const ElfW(Phdr) *phdr_table, size_t phdr_count, ElfW(Addr) load_bias) { - return _phdr_table_set_load_prot(phdr_table, phdr_count, load_bias, 0); + return _phdr_table_set_load_prot(phdr_table, phdr_count, load_bias, 0); } /* Change the protection of all loaded segments in memory to writable. @@ -204,28 +222,28 @@ int phdr_table_protect_segments(const ElfW(Phdr) *phdr_table, size_t phdr_count, * 0 on error, -1 on failure (error code in errno). */ int phdr_table_unprotect_segments(const ElfW(Phdr) *phdr_table, size_t phdr_count, ElfW(Addr) load_bias) { - return _phdr_table_set_load_prot(phdr_table, phdr_count, load_bias, PROT_WRITE); + return _phdr_table_set_load_prot(phdr_table, phdr_count, load_bias, PROT_WRITE); } uint32_t calculate_elf_hash(const char *name) { - const uint8_t *name_bytes = (const uint8_t *) name; - uint32_t h = 0, g; - - while (*name_bytes) { - h = (h << 4) + *name_bytes++; - g = h & 0xf0000000; - h ^= g; - h ^= g >> 24; - } - return h; + const uint8_t *name_bytes = (const uint8_t *) name; + uint32_t h = 0, g; + + while (*name_bytes) { + h = (h << 4) + *name_bytes++; + g = h & 0xf0000000; + h ^= g; + h ^= g >> 24; + } + return h; } uint32_t calculate_gnu_hash(const char *name) { - const uint8_t *name_bytes = (const uint8_t *) name; - uint32_t h = 5381; + const uint8_t *name_bytes = (const uint8_t *) name; + uint32_t h = 5381; - while (*name_bytes != 0) { - h += (h << 5) + *name_bytes++; - } - return h; + while (*name_bytes != 0) { + h += (h << 5) + *name_bytes++; + } + return h; } \ No newline at end of file diff --git a/src/main/cpp/linker_main.cpp b/src/main/cpp/linker_main.cpp index 03c3c5f..526291d 100644 --- a/src/main/cpp/linker_main.cpp +++ b/src/main/cpp/linker_main.cpp @@ -64,7 +64,7 @@ Java_com_sanfengandroid_fakelinker_FakeLinker_entrance(JNIEnv *env, jclass clazz #if __ANDROID_API__ >= __ANDROID_API_N__ soinfo *so = ProxyLinker::SoinfoFromHandle(hookHandle); std::string value = android_namespace_to_string(so->get_primary_namespace()); - LOGD("This handle: %p, namespace: %s", hookHandle, value.c_str()); + LOGV("This handle: %p, namespace: %s", hookHandle, value.c_str()); #endif InitHookModule(env, hookHandle, cache.c_str(), config.c_str(), process_.c_str()); return JNI_TRUE; @@ -74,7 +74,6 @@ extern "C" JNIEXPORT jint JNICALL Java_com_sanfengandroid_fakelinker_FakeLinker_ ScopedUtfChars symbol(env, symbol_name); int error_code; - LOGD("Java invoke %s relink symbol '%s'", add ? "add" : "remove", symbol.c_str()); gRemoteInvokeInterface.CallCommonFunction(add ? kCFAddRelinkFilterSymbol : kCFRemoveRelinkFilterSymbol, kSPSymbol, symbol.c_str(), kSPNull, nullptr, &error_code); if (error_code != kErrorNo) { LOGE("Add or remove relink symbol '%s' failed, error code: %d", symbol.c_str(), error_code); diff --git a/src/main/cpp/linker_main.h b/src/main/cpp/linker_main.h index eb69d8f..169e5fa 100644 --- a/src/main/cpp/linker_main.h +++ b/src/main/cpp/linker_main.h @@ -7,11 +7,11 @@ #include -API_PUBLIC void *call_soinfo_function(SoinfoFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code); +C_API API_PUBLIC void *call_soinfo_function(SoinfoFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code); -API_PUBLIC void *call_common_function(CommonFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code); +C_API API_PUBLIC void *call_common_function(CommonFunType fun_type, SoinfoParamType find_type, const void *find_param, SoinfoParamType param_type, const void *param, int *error_code); #if __ANDROID_API__ >= __ANDROID_API_N__ -API_PUBLIC void * +C_API API_PUBLIC void * call_namespace_function(NamespaceFunType fun_type, NamespaceParamType find_type, const void *find_param, NamespaceParamType param_type, const void *param, int *error_code); #endif \ No newline at end of file diff --git a/src/main/cpp/symbol.map.txt b/src/main/cpp/symbol.map.txt new file mode 100644 index 0000000..c616c36 --- /dev/null +++ b/src/main/cpp/symbol.map.txt @@ -0,0 +1,11 @@ +{ + global: + Java_com_sanfengandroid_fakelinker_FakeLinker_entrance; + Java_com_sanfengandroid_fakelinker_FakeLinker_relinkSpecialFilterSymbol; + Java_com_sanfengandroid_fakelinker_FakeLinker_setLogLevel; + call_soinfo_function; + call_common_function; + call_namespace_function; + local: + *; +}; \ No newline at end of file diff --git a/src/main/java/eu/chainfire/libsuperuser/Application.java b/src/main/java/eu/chainfire/libsuperuser/Application.java new file mode 100644 index 0000000..3fd58f0 --- /dev/null +++ b/src/main/java/eu/chainfire/libsuperuser/Application.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2012-2015 Jorrit "Chainfire" Jongma + * + * 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. + */ + +package eu.chainfire.libsuperuser; + +import android.content.Context; +import android.os.Handler; +import android.widget.Toast; + +/** + * Base application class to extend from, solving some issues with + * toasts and AsyncTasks you are likely to run into + */ +public class Application extends android.app.Application { + /** + * Shows a toast message + * + * @param context Any context belonging to this application + * @param message The message to show + */ + public static void toast(Context context, String message) { + // this is a static method so it is easier to call, + // as the context checking and casting is done for you + + if (context == null) return; + + if (!(context instanceof Application)) { + context = context.getApplicationContext(); + } + + if (context instanceof Application) { + final Context c = context; + final String m = message; + + ((Application) context).runInApplicationThread(new Runnable() { + @Override + public void run() { + Toast.makeText(c, m, Toast.LENGTH_LONG).show(); + } + }); + } + } + + private static Handler mApplicationHandler = new Handler(); + + /** + * Run a runnable in the main application thread + * + * @param r Runnable to run + */ + public void runInApplicationThread(Runnable r) { + mApplicationHandler.post(r); + } + + @Override + public void onCreate() { + super.onCreate(); + + try { + // workaround bug in AsyncTask, can show up (for example) when you toast from a service + // this makes sure AsyncTask's internal handler is created from the right (main) thread + Class.forName("android.os.AsyncTask"); + } catch (ClassNotFoundException e) { + // will never happen + } + } +} diff --git a/src/main/java/eu/chainfire/libsuperuser/Debug.java b/src/main/java/eu/chainfire/libsuperuser/Debug.java new file mode 100644 index 0000000..580f254 --- /dev/null +++ b/src/main/java/eu/chainfire/libsuperuser/Debug.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2012-2015 Jorrit "Chainfire" Jongma + * + * 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. + */ + +package eu.chainfire.libsuperuser; + +import android.os.Looper; +import android.util.Log; + +import com.sanfengandroid.fakelinker.BuildConfig; + +/** + * Utility class for logging and debug features that (by default) does nothing when not in debug mode + */ +public class Debug { + + // ----- DEBUGGING ----- + + private static boolean debug = BuildConfig.DEBUG; + + /** + *

Enable or disable debug mode

+ * + *

By default, debug mode is enabled for development + * builds and disabled for exported APKs - see + * BuildConfig.DEBUG

+ * + * @param enable Enable debug mode ? + */ + public static void setDebug(boolean enable) { + debug = enable; + } + + /** + *

Is debug mode enabled ?

+ * + * @return Debug mode enabled + */ + public static boolean getDebug() { + return debug; + } + + // ----- LOGGING ----- + + public interface OnLogListener { + void onLog(int type, String typeIndicator, String message); + } + + public static final String TAG = "libsuperuser"; + + public static final int LOG_GENERAL = 0x0001; + public static final int LOG_COMMAND = 0x0002; + public static final int LOG_OUTPUT = 0x0004; + + public static final int LOG_NONE = 0x0000; + public static final int LOG_ALL = 0xFFFF; + + private static int logTypes = LOG_ALL; + + private static OnLogListener logListener = null; + + /** + *

Log a message (internal)

+ * + *

Current debug and enabled logtypes decide what gets logged - + * even if a custom callback is registered

+ * + * @param type Type of message to log + * @param typeIndicator String indicator for message type + * @param message The message to log + */ + private static void logCommon(int type, String typeIndicator, String message) { + if (debug && ((logTypes & type) == type)) { + if (logListener != null) { + logListener.onLog(type, typeIndicator, message); + } else { + Log.d(TAG, "[" + TAG + "][" + typeIndicator + "]" + (!message.startsWith("[") && !message.startsWith(" ") ? " " : "") + message); + } + } + } + + /** + *

Log a "general" message

+ * + *

These messages are infrequent and mostly occur at startup/shutdown or on error

+ * + * @param message The message to log + */ + public static void log(String message) { + logCommon(LOG_GENERAL, "G", message); + } + + /** + *

Log a "per-command" message

+ * + *

This could produce a lot of output if the client runs many commands in the session

+ * + * @param message The message to log + */ + public static void logCommand(String message) { + logCommon(LOG_COMMAND, "C", message); + } + + /** + *

Log a line of stdout/stderr output

+ * + *

This could produce a lot of output if the shell commands are noisy

+ * + * @param message The message to log + */ + public static void logOutput(String message) { + logCommon(LOG_OUTPUT, "O", message); + } + + /** + *

Enable or disable logging specific types of message

+ * + *

You may | (or) LOG_* constants together. Note that + * debug mode must also be enabled for actual logging to + * occur.

+ * + * @param type LOG_* constants + * @param enable Enable or disable + */ + public static void setLogTypeEnabled(int type, boolean enable) { + if (enable) { + logTypes |= type; + } else { + logTypes &= ~type; + } + } + + /** + *

Is logging for specific types of messages enabled ?

+ * + *

You may | (or) LOG_* constants together, to learn if + * all passed message types are enabled for logging. Note + * that debug mode must also be enabled for actual logging + * to occur.

+ * + * @param type LOG_* constants + * @return enabled? + */ + public static boolean getLogTypeEnabled(int type) { + return ((logTypes & type) == type); + } + + /** + *

Is logging for specific types of messages enabled ?

+ * + *

You may | (or) LOG_* constants together, to learn if + * all message types are enabled for logging. Takes + * debug mode into account for the result.

+ * + * @param type LOG_* constants + * @return enabled and in debug mode? + */ + public static boolean getLogTypeEnabledEffective(int type) { + return getDebug() && getLogTypeEnabled(type); + } + + /** + *

Register a custom log handler

+ * + *

Replaces the log method (write to logcat) with your own + * handler. Whether your handler gets called is still dependent + * on debug mode and message types being enabled for logging.

+ * + * @param onLogListener Custom log listener or NULL to revert to default + */ + public static void setOnLogListener(OnLogListener onLogListener) { + logListener = onLogListener; + } + + /** + *

Get the currently registered custom log handler

+ * + * @return Current custom log handler or NULL if none is present + */ + public static OnLogListener getOnLogListener() { + return logListener; + } + + // ----- SANITY CHECKS ----- + + private static boolean sanityChecks = true; + + /** + *

Enable or disable sanity checks

+ * + *

Enables or disables the library crashing when su is called + * from the main thread.

+ * + * @param enable Enable or disable + */ + public static void setSanityChecksEnabled(boolean enable) { + sanityChecks = enable; + } + + /** + *

Are sanity checks enabled ?

+ * + *

Note that debug mode must also be enabled for actual + * sanity checks to occur.

+ * + * @return True if enabled + */ + public static boolean getSanityChecksEnabled() { + return sanityChecks; + } + + /** + *

Are sanity checks enabled ?

+ * + *

Takes debug mode into account for the result.

+ * + * @return True if enabled + */ + public static boolean getSanityChecksEnabledEffective() { + return getDebug() && getSanityChecksEnabled(); + } + + /** + *

Are we running on the main thread ?

+ * + * @return Running on main thread ? + */ + public static boolean onMainThread() { + return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())); + } + +} diff --git a/src/main/java/eu/chainfire/libsuperuser/HideOverlaysReceiver.java b/src/main/java/eu/chainfire/libsuperuser/HideOverlaysReceiver.java new file mode 100644 index 0000000..265918a --- /dev/null +++ b/src/main/java/eu/chainfire/libsuperuser/HideOverlaysReceiver.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2012-2015 Jorrit "Chainfire" Jongma + * + * 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. + */ + +package eu.chainfire.libsuperuser; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + *

+ * Base receiver to extend to catch notifications when overlays should be + * hidden. + *

+ *

+ * Tapjacking protection in SuperSU prevents some dialogs from receiving user + * input when overlays are present. For security reasons this notification is + * only sent to apps that have previously been granted root access, so even if + * your app does not require root, you still need to request + * it, and the user must grant it. + *

+ *

+ * Note that the word overlay as used here should be interpreted as "any view or + * window possibly obscuring SuperSU dialogs". + *

+ */ +public abstract class HideOverlaysReceiver extends BroadcastReceiver { + public static final String ACTION_HIDE_OVERLAYS = "eu.chainfire.supersu.action.HIDE_OVERLAYS"; + public static final String CATEGORY_HIDE_OVERLAYS = Intent.CATEGORY_INFO; + public static final String EXTRA_HIDE_OVERLAYS = "eu.chainfire.supersu.extra.HIDE"; + + @Override + public final void onReceive(Context context, Intent intent) { + if (intent.hasExtra(EXTRA_HIDE_OVERLAYS)) { + onHideOverlays(context, intent, intent.getBooleanExtra(EXTRA_HIDE_OVERLAYS, false)); + } + } + + /** + * Called when overlays should be hidden or may be shown + * again. + * + * @param context App context + * @param intent Received intent + * @param hide Should overlays be hidden? + */ + public abstract void onHideOverlays(Context context, Intent intent, boolean hide); +} diff --git a/src/main/java/eu/chainfire/libsuperuser/Policy.java b/src/main/java/eu/chainfire/libsuperuser/Policy.java new file mode 100644 index 0000000..1c9b688 --- /dev/null +++ b/src/main/java/eu/chainfire/libsuperuser/Policy.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2012-2015 Jorrit "Chainfire" Jongma + * + * 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. + */ + +package eu.chainfire.libsuperuser; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for modifying SELinux policies, reducing the number of calls to a minimum. + * + * Example usage: + * + *
+ * 
+ *
+ * private class Policy extends eu.chainfire.libsuperuser.Policy {
+ *     {@literal @}Override protected String[] getPolicies() {
+ *         return new String[] {
+ *             "allow sdcardd unlabeled dir { append create execute write relabelfrom link unlink ioctl getattr setattr read rename lock mounton quotaon swapon rmdir audit_access remove_name add_name reparent execmod search open }",
+ *             "allow sdcardd unlabeled file { append create write relabelfrom link unlink ioctl getattr setattr read rename lock mounton quotaon swapon audit_access open }",
+ *             "allow unlabeled unlabeled filesystem associate"
+ *         };
+ *     }
+ * };
+ * private Policy policy = new Policy();
+ *
+ * public void someFunctionNotCalledOnMainThread() {
+ *     policy.inject();
+ * }
+ *
+ * 
+ * 
+ */ +public abstract class Policy { + /** + * supolicy should be called as little as possible. We batch policies together. The command + * line is guaranteed to be able to take 4096 characters. Reduce by a bit for supolicy itself. + */ + private static final int MAX_POLICY_LENGTH = 4096 - 32; + + private static final Object synchronizer = new Object(); + private static volatile Boolean canInject = null; + private static volatile boolean injected = false; + + /** + * @return Have we injected our policies already? + */ + public static boolean haveInjected() { + return injected; + } + + /** + * Reset policies-have-been-injected state, if you really need to inject them again. Extremely + * rare, you will probably never need this. + */ + public static void resetInjected() { + synchronized (synchronizer) { + injected = false; + } + } + + /** + * Override this method to return a array of strings containing the policies you want to inject. + * + * @return Policies to inject + */ + protected abstract String[] getPolicies(); + + /** + * Detects availability of the supolicy tool. Only useful if Shell.SU.isSELinuxEnforcing() + * returns true. + * + * @return canInject? + */ + public static boolean canInject() { + synchronized (synchronizer) { + if (canInject != null) return canInject; + + canInject = false; + + // We are making the assumption here that if supolicy is called without parameters, + // it will return output (such as a usage notice) on STDOUT (not STDERR) that contains + // at least the word "supolicy". This is true at least for SuperSU. + + List result = Shell.run("sh", new String[] { "supolicy" }, null, false); + if (result != null) { + for (String line : result) { + if (line.contains("supolicy")) { + canInject = true; + break; + } + } + } + + return canInject; + } + } + + /** + * Reset cached can-inject state and force redetection on nect canInject() call + */ + public static void resetCanInject() { + synchronized (synchronizer) { + canInject = null; + } + } + + /** + * Transform the policies defined by getPolicies() into a set of shell commands + * + * @return Possibly empty List of commands, or null + */ + protected List getInjectCommands() { + return getInjectCommands(true); + } + + /** + * Transform the policies defined by getPolicies() into a set of shell commands + * + * @param allowBlocking allow method to perform blocking I/O for extra checks + * @return Possibly empty List of commands, or null + */ + protected List getInjectCommands(boolean allowBlocking) { + synchronized (synchronizer) { + // No reason to bother if we're in permissive mode + if (!Shell.SU.isSELinuxEnforcing()) return null; + + // If we can't inject, no use continuing + if (allowBlocking && !canInject()) return null; + + // Been there, done that + if (injected) return null; + + // Retrieve policies + String[] policies = getPolicies(); + if ((policies != null) && (policies.length > 0)) { + List commands = new ArrayList(); + + // Combine the policies into a minimal number of commands + String command = ""; + for (String policy : policies) { + if ((command.length() == 0) || (command.length() + policy.length() + 3 < MAX_POLICY_LENGTH)) { + command = command + " \"" + policy + "\""; + } else { + commands.add("supolicy --live" + command); + command = ""; + } + } + if (command.length() > 0) { + commands.add("supolicy --live" + command); + } + + return commands; + } + + // No policies + return null; + } + } + + /** + * Inject the policies defined by getPolicies(). Throws an exception if called from + * the main thread in debug mode. + */ + public void inject() { + synchronized (synchronizer) { + // Get commands that inject our policies + List commands = getInjectCommands(); + + // Execute them, if any + if ((commands != null) && (commands.size() > 0)) { + Shell.SU.run(commands); + } + + // We survived without throwing + injected = true; + } + } + + /** + * Inject the policies defined by getPolicies(). Throws an exception if called from + * the main thread in debug mode if waitForIdle is true. If waitForIdle is false + * however, it cannot be guaranteed the command was executed and the policies injected + * upon return. + * + * @param shell Interactive shell to execute commands on + * @param waitForIdle wait for the command to complete before returning ? + */ + public void inject(Shell.Interactive shell, boolean waitForIdle) { + synchronized (synchronizer) { + // Get commands that inject our policies + List commands = getInjectCommands(waitForIdle); + + // Execute them, if any + if ((commands != null) && (commands.size() > 0)) { + shell.addCommand(commands); + if (waitForIdle) { + shell.waitForIdle(); + } + } + + // We survived without throwing + injected = true; + } + } +} diff --git a/src/main/java/eu/chainfire/libsuperuser/Shell.java b/src/main/java/eu/chainfire/libsuperuser/Shell.java new file mode 100644 index 0000000..6bf4bd4 --- /dev/null +++ b/src/main/java/eu/chainfire/libsuperuser/Shell.java @@ -0,0 +1,1806 @@ +/* + * Copyright (C) 2012-2015 Jorrit "Chainfire" Jongma + * + * 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. + */ + +package eu.chainfire.libsuperuser; + +import android.os.Handler; +import android.os.Looper; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import eu.chainfire.libsuperuser.StreamGobbler.OnLineListener; + +/** + * Class providing functionality to execute commands in a (root) shell + */ +public class Shell { + /** + *

+ * Runs commands using the supplied shell, and returns the output, or null + * in case of errors. + *

+ *

+ * This method is deprecated and only provided for backwards compatibility. + * Use {@link #run(String, String[], String[], boolean)} instead, and see + * that same method for usage notes. + *

+ * + * @param shell The shell to use for executing the commands + * @param commands The commands to execute + * @param wantSTDERR Return STDERR in the output ? + * @return Output of the commands, or null in case of an error + */ + @Deprecated + public static List run(String shell, String[] commands, boolean wantSTDERR) { + return run(shell, commands, null, wantSTDERR); + } + + /** + *

+ * Runs commands using the supplied shell, and returns the output, or null + * in case of errors. + *

+ *

+ * Note that due to compatibility with older Android versions, wantSTDERR is + * not implemented using redirectErrorStream, but rather appended to the + * output. STDOUT and STDERR are thus not guaranteed to be in the correct + * order in the output. + *

+ *

+ * Note as well that this code will intentionally crash when run in debug + * mode from the main thread of the application. You should always execute + * shell commands from a background thread. + *

+ *

+ * When in debug mode, the code will also excessively log the commands + * passed to and the output returned from the shell. + *

+ *

+ * Though this function uses background threads to gobble STDOUT and STDERR + * so a deadlock does not occur if the shell produces massive output, the + * output is still stored in a List<String>, and as such doing + * something like 'ls -lR /' will probably have you run out of + * memory. + *

+ * + * @param shell The shell to use for executing the commands + * @param commands The commands to execute + * @param environment List of all environment variables (in 'key=value' + * format) or null for defaults + * @param wantSTDERR Return STDERR in the output ? + * @return Output of the commands, or null in case of an error + */ + public static List run(String shell, String[] commands, String[] environment, + boolean wantSTDERR) { + String shellUpper = shell.toUpperCase(Locale.ENGLISH); + + if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { + // check if we're running in the main thread, and if so, crash if + // we're in debug mode, to let the developer know attention is + // needed here. + + Debug.log(ShellOnMainThreadException.EXCEPTION_COMMAND); + throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_COMMAND); + } + Debug.logCommand(String.format("[%s%%] START", shellUpper)); + + List res = Collections.synchronizedList(new ArrayList()); + + try { + // Combine passed environment with system environment + if (environment != null) { + Map newEnvironment = new HashMap(); + newEnvironment.putAll(System.getenv()); + int split; + for (String entry : environment) { + if ((split = entry.indexOf("=")) >= 0) { + newEnvironment.put(entry.substring(0, split), entry.substring(split + 1)); + } + } + int i = 0; + environment = new String[newEnvironment.size()]; + for (Map.Entry entry : newEnvironment.entrySet()) { + environment[i] = entry.getKey() + "=" + entry.getValue(); + i++; + } + } + + // setup our process, retrieve STDIN stream, and STDOUT/STDERR + // gobblers + Process process = Runtime.getRuntime().exec(shell, environment); + DataOutputStream STDIN = new DataOutputStream(process.getOutputStream()); + StreamGobbler STDOUT = new StreamGobbler(shellUpper + "-", process.getInputStream(), + res); + StreamGobbler STDERR = new StreamGobbler(shellUpper + "*", process.getErrorStream(), + wantSTDERR ? res : null); + + // start gobbling and write our commands to the shell + STDOUT.start(); + STDERR.start(); + try { + for (String write : commands) { + Debug.logCommand(String.format("[%s+] %s", shellUpper, write)); + STDIN.write((write + "\n").getBytes("UTF-8")); + STDIN.flush(); + } + STDIN.write("exit\n".getBytes("UTF-8")); + STDIN.flush(); + } catch (IOException e) { + if (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed")) { + // Method most horrid to catch broken pipe, in which case we + // do nothing. The command is not a shell, the shell closed + // STDIN, the script already contained the exit command, etc. + // these cases we want the output instead of returning null. + } else { + // other issues we don't know how to handle, leads to + // returning null + throw e; + } + } + + // wait for our process to finish, while we gobble away in the + // background + process.waitFor(); + + // make sure our threads are done gobbling, our streams are closed, + // and the process is destroyed - while the latter two shouldn't be + // needed in theory, and may even produce warnings, in "normal" Java + // they are required for guaranteed cleanup of resources, so lets be + // safe and do this on Android as well + try { + STDIN.close(); + } catch (IOException e) { + // might be closed already + } + STDOUT.join(); + STDERR.join(); + process.destroy(); + + // in case of su, 255 usually indicates access denied + if (SU.isSU(shell) && (process.exitValue() == 255)) { + res = null; + } + } catch (IOException e) { + // shell probably not found + res = null; + } catch (InterruptedException e) { + // this should really be re-thrown + res = null; + } + + Debug.logCommand(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH))); + return res; + } + + protected static String[] availableTestCommands = new String[]{ + "echo -BOC-", + "id" + }; + + /** + * See if the shell is alive, and if so, check the UID + * + * @param ret Standard output from running availableTestCommands + * @param checkForRoot true if we are expecting this shell to be running as + * root + * @return true on success, false on error + */ + protected static boolean parseAvailableResult(List ret, boolean checkForRoot) { + if (ret == null) + return false; + + // this is only one of many ways this can be done + boolean echo_seen = false; + + for (String line : ret) { + if (line.contains("uid=")) { + // id command is working, let's see if we are actually root + return !checkForRoot || line.contains("uid=0"); + } else if (line.contains("-BOC-")) { + // if we end up here, at least the su command starts some kind + // of shell, let's hope it has root privileges - no way to know without + // additional native binaries + echo_seen = true; + } + } + + return echo_seen; + } + + /** + * This class provides utility functions to easily execute commands using SH + */ + public static class SH { + /** + * Runs command and return output + * + * @param command The command to run + * @return Output of the command, or null in case of an error + */ + public static List run(String command) { + return Shell.run("sh", new String[]{ + command + }, null, false); + } + + /** + * Runs commands and return output + * + * @param commands The commands to run + * @return Output of the commands, or null in case of an error + */ + public static List run(List commands) { + return Shell.run("sh", commands.toArray(new String[commands.size()]), null, false); + } + + /** + * Runs commands and return output + * + * @param commands The commands to run + * @return Output of the commands, or null in case of an error + */ + public static List run(String[] commands) { + return Shell.run("sh", commands, null, false); + } + } + + /** + * This class provides utility functions to easily execute commands using SU + * (root shell), as well as detecting whether or not root is available, and + * if so which version. + */ + public static class SU { + private static Boolean isSELinuxEnforcing = null; + private static String[] suVersion = new String[]{ + null, null + }; + + /** + * Runs command as root (if available) and return output + * + * @param command The command to run + * @return Output of the command, or null if root isn't available or in + * case of an error + */ + public static List run(String command) { + return Shell.run("su", new String[]{ + command + }, null, false); + } + + /** + * Runs commands as root (if available) and return output + * + * @param commands The commands to run + * @return Output of the commands, or null if root isn't available or in + * case of an error + */ + public static List run(List commands) { + return Shell.run("su", commands.toArray(new String[commands.size()]), null, false); + } + + /** + * Runs commands as root (if available) and return output + * + * @param commands The commands to run + * @return Output of the commands, or null if root isn't available or in + * case of an error + */ + public static List run(String[] commands) { + return Shell.run("su", commands, null, false); + } + + /** + * Detects whether or not superuser access is available, by checking the + * output of the "id" command if available, checking if a shell runs at + * all otherwise + * + * @return True if superuser access available + */ + public static boolean available() { + // this is only one of many ways this can be done + + List ret = run(Shell.availableTestCommands); + return Shell.parseAvailableResult(ret, true); + } + + /** + *

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

+ *

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

+ *

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

+ * + * @param internal Request human-readable version or application + * internal version + * @return String containing the su version or null + */ + public static synchronized String version(boolean internal) { + int idx = internal ? 0 : 1; + if (suVersion[idx] == null) { + String version = null; + + List ret = Shell.run( + internal ? "su -V" : "su -v", + new String[] { "exit" }, + null, + false + ); + + if (ret != null) { + for (String line : ret) { + if (!internal) { + if (!line.trim().equals("")) { + version = line; + break; + } + } else { + try { + if (Integer.parseInt(line) > 0) { + version = line; + break; + } + } catch (NumberFormatException e) { + // should be parsable, try next line otherwise + } + } + } + } + + suVersion[idx] = version; + } + return suVersion[idx]; + } + + /** + * Attempts to deduce if the shell command refers to a su shell + * + * @param shell Shell command to run + * @return Shell command appears to be su + */ + public static boolean isSU(String shell) { + // Strip parameters + int pos = shell.indexOf(' '); + if (pos >= 0) { + shell = shell.substring(0, pos); + } + + // Strip path + pos = shell.lastIndexOf('/'); + if (pos >= 0) { + shell = shell.substring(pos + 1); + } + + return shell.equals("su"); + } + + /** + * Constructs a shell command to start a su shell using the supplied uid + * and SELinux context. This is can be an expensive operation, consider + * caching the result. + * + * @param uid Uid to use (0 == root) + * @param context (SELinux) context name to use or null + * @return Shell command + */ + public static String shell(int uid, String context) { + // su[ --context ][ ] + String shell = "su"; + + if ((context != null) && isSELinuxEnforcing()) { + String display = version(false); + String internal = version(true); + + // We only know the format for SuperSU v1.90+ right now + //TODO add detection for other su's that support this + if ((display != null) && + (internal != null) && + (display.endsWith("SUPERSU")) && + (Integer.valueOf(internal) >= 190)) { + shell = String.format(Locale.ENGLISH, "%s --context %s", shell, context); + } + } + + // Most su binaries support the "su " format, but in case + // they don't, lets skip it for the default 0 (root) case + if (uid > 0) { + shell = String.format(Locale.ENGLISH, "%s %d", shell, uid); + } + + return shell; + } + + /** + * Constructs a shell command to start a su shell connected to mount + * master daemon, to perform public mounts on Android 4.3+ (or 4.2+ in + * SELinux enforcing mode) + * + * @return Shell command + */ + public static String shellMountMaster() { + if (android.os.Build.VERSION.SDK_INT >= 17) { + return "su --mount-master"; + } + return "su"; + } + + /** + * Detect if SELinux is set to enforcing, caches result + * + * @return true if SELinux set to enforcing, or false in the case of + * permissive or not present + */ + public static synchronized boolean isSELinuxEnforcing() { + if (isSELinuxEnforcing == null) { + Boolean enforcing = null; + + // First known firmware with SELinux built-in was a 4.2 (17) + // leak + if (android.os.Build.VERSION.SDK_INT >= 17) { + if (android.os.Build.VERSION.SDK_INT >= 28) { + // Due to non-SDK API greylisting, we cannot determine SELinux status + // through the methods below, so we assume SELinux is enforcing and + // potentially patch policies for nothing + enforcing = true; + } + + // Detect enforcing through sysfs, not always present + if (enforcing == null) { + File f = new File("/sys/fs/selinux/enforce"); + if (f.exists()) { + try { + InputStream is = new FileInputStream("/sys/fs/selinux/enforce"); + try { + enforcing = (is.read() == '1'); + } finally { + is.close(); + } + } catch (Exception e) { + // we might not be allowed to read, thanks SELinux + } + } + } + + // 4.4+ has a new API to detect SELinux mode, so use it + // SELinux is typically in enforced mode, but emulators may have SELinux disabled + if (enforcing == null) { + try { + Class seLinux = Class.forName("android.os.SELinux"); + Method isSELinuxEnforced = seLinux.getMethod("isSELinuxEnforced"); + enforcing = (Boolean) isSELinuxEnforced.invoke(seLinux.newInstance()); + } catch (Exception e) { + // 4.4+ release builds are enforcing by default, take the gamble + enforcing = (android.os.Build.VERSION.SDK_INT >= 19); + } + } + } + + if (enforcing == null) { + enforcing = false; + } + + isSELinuxEnforcing = enforcing; + } + return isSELinuxEnforcing; + } + + /** + *

+ * Clears results cached by isSELinuxEnforcing() and version(boolean + * internal) calls. + *

+ *

+ * Most apps should never need to call this, as neither enforcing status + * nor su version is likely to change on a running device - though it is + * not impossible. + *

+ */ + public static synchronized void clearCachedResults() { + isSELinuxEnforcing = null; + suVersion[0] = null; + suVersion[1] = null; + } + } + + private interface OnResult { + // for any onCommandResult callback + int WATCHDOG_EXIT = -1; + int SHELL_DIED = -2; + + // for Interactive.open() callbacks only + int SHELL_EXEC_FAILED = -3; + int SHELL_WRONG_UID = -4; + int SHELL_RUNNING = 0; + } + + /** + * Command result callback, notifies the recipient of the completion of a + * command block, including the (last) exit code, and the full output + */ + public interface OnCommandResultListener extends OnResult { + /** + *

+ * Command result callback + *

+ *

+ * Depending on how and on which thread the shell was created, this + * callback may be executed on one of the gobbler threads. In that case, + * it is important the callback returns as quickly as possible, as + * delays in this callback may pause the native process or even result + * in a deadlock + *

+ *

+ * See {@link Interactive} for threading details + *

+ * + * @param commandCode Value previously supplied to addCommand + * @param exitCode Exit code of the last command in the block + * @param output All output generated by the command block + */ + void onCommandResult(int commandCode, int exitCode, List output); + } + + /** + * Command per line callback for parsing the output line by line without + * buffering It also notifies the recipient of the completion of a command + * block, including the (last) exit code. + */ + public interface OnCommandLineListener extends OnResult, OnLineListener { + /** + *

+ * Command result callback + *

+ *

+ * Depending on how and on which thread the shell was created, this + * callback may be executed on one of the gobbler threads. In that case, + * it is important the callback returns as quickly as possible, as + * delays in this callback may pause the native process or even result + * in a deadlock + *

+ *

+ * See {@link Interactive} for threading details + *

+ * + * @param commandCode Value previously supplied to addCommand + * @param exitCode Exit code of the last command in the block + */ + void onCommandResult(int commandCode, int exitCode); + } + + /** + * Internal class to store command block properties + */ + private static class Command { + private static int commandCounter = 0; + + private final String[] commands; + private final int code; + private final OnCommandResultListener onCommandResultListener; + private final OnCommandLineListener onCommandLineListener; + private final String marker; + + public Command(String[] commands, int code, + OnCommandResultListener onCommandResultListener, + OnCommandLineListener onCommandLineListener) { + this.commands = commands; + this.code = code; + this.onCommandResultListener = onCommandResultListener; + this.onCommandLineListener = onCommandLineListener; + this.marker = UUID.randomUUID().toString() + String.format("-%08x", ++commandCounter); + } + } + + /** + * Builder class for {@link Interactive} + */ + public static class Builder { + private Handler handler = null; + private boolean autoHandler = true; + private String shell = "sh"; + private boolean wantSTDERR = false; + private List commands = new LinkedList(); + private Map environment = new HashMap(); + private OnLineListener onSTDOUTLineListener = null; + private OnLineListener onSTDERRLineListener = null; + private int watchdogTimeout = 0; + + /** + *

+ * Set a custom handler that will be used to post all callbacks to + *

+ *

+ * See {@link Interactive} for further details on threading and + * handlers + *

+ * + * @param handler Handler to use + * @return This Builder object for method chaining + */ + public Builder setHandler(Handler handler) { + this.handler = handler; + return this; + } + + /** + *

+ * Automatically create a handler if possible ? Default to true + *

+ *

+ * See {@link Interactive} for further details on threading and + * handlers + *

+ * + * @param autoHandler Auto-create handler ? + * @return This Builder object for method chaining + */ + public Builder setAutoHandler(boolean autoHandler) { + this.autoHandler = autoHandler; + return this; + } + + /** + * Set shell binary to use. Usually "sh" or "su", do not use a full path + * unless you have a good reason to + * + * @param shell Shell to use + * @return This Builder object for method chaining + */ + public Builder setShell(String shell) { + this.shell = shell; + return this; + } + + /** + * Convenience function to set "sh" as used shell + * + * @return This Builder object for method chaining + */ + public Builder useSH() { + return setShell("sh"); + } + + /** + * Convenience function to set "su" as used shell + * + * @return This Builder object for method chaining + */ + public Builder useSU() { + return setShell("su"); + } + + /** + * Set if error output should be appended to command block result output + * + * @param wantSTDERR Want error output ? + * @return This Builder object for method chaining + */ + public Builder setWantSTDERR(boolean wantSTDERR) { + this.wantSTDERR = wantSTDERR; + return this; + } + + /** + * Add or update an environment variable + * + * @param key Key of the environment variable + * @param value Value of the environment variable + * @return This Builder object for method chaining + */ + public Builder addEnvironment(String key, String value) { + environment.put(key, value); + return this; + } + + /** + * Add or update environment variables + * + * @param addEnvironment Map of environment variables + * @return This Builder object for method chaining + */ + public Builder addEnvironment(Map addEnvironment) { + environment.putAll(addEnvironment); + return this; + } + + /** + * Add a command to execute + * + * @param command Command to execute + * @return This Builder object for method chaining + */ + public Builder addCommand(String command) { + return addCommand(command, 0, null); + } + + /** + *

+ * Add a command to execute, with a callback to be called on completion + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param command Command to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion + * @return This Builder object for method chaining + */ + public Builder addCommand(String command, int code, + OnCommandResultListener onCommandResultListener) { + return addCommand(new String[]{ + command + }, code, onCommandResultListener); + } + + /** + * Add commands to execute + * + * @param commands Commands to execute + * @return This Builder object for method chaining + */ + public Builder addCommand(List commands) { + return addCommand(commands, 0, null); + } + + /** + *

+ * Add commands to execute, with a callback to be called on completion + * (of all commands) + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion + * (of all commands) + * @return This Builder object for method chaining + */ + public Builder addCommand(List commands, int code, + OnCommandResultListener onCommandResultListener) { + return addCommand(commands.toArray(new String[commands.size()]), code, + onCommandResultListener); + } + + /** + * Add commands to execute + * + * @param commands Commands to execute + * @return This Builder object for method chaining + */ + public Builder addCommand(String[] commands) { + return addCommand(commands, 0, null); + } + + /** + *

+ * Add commands to execute, with a callback to be called on completion + * (of all commands) + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion + * (of all commands) + * @return This Builder object for method chaining + */ + public Builder addCommand(String[] commands, int code, + OnCommandResultListener onCommandResultListener) { + this.commands.add(new Command(commands, code, onCommandResultListener, null)); + return this; + } + + /** + *

+ * Set a callback called for every line output to STDOUT by the shell + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param onLineListener Callback to be called for each line + * @return This Builder object for method chaining + */ + public Builder setOnSTDOUTLineListener(OnLineListener onLineListener) { + this.onSTDOUTLineListener = onLineListener; + return this; + } + + /** + *

+ * Set a callback called for every line output to STDERR by the shell + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param onLineListener Callback to be called for each line + * @return This Builder object for method chaining + */ + public Builder setOnSTDERRLineListener(OnLineListener onLineListener) { + this.onSTDERRLineListener = onLineListener; + return this; + } + + /** + *

+ * Enable command timeout callback + *

+ *

+ * This will invoke the onCommandResult() callback with exitCode + * WATCHDOG_EXIT if a command takes longer than watchdogTimeout seconds + * to complete. + *

+ *

+ * If a watchdog timeout occurs, it generally means that the Interactive + * session is out of sync with the shell process. The caller should + * close the current session and open a new one. + *

+ * + * @param watchdogTimeout Timeout, in seconds; 0 to disable + * @return This Builder object for method chaining + */ + public Builder setWatchdogTimeout(int watchdogTimeout) { + this.watchdogTimeout = watchdogTimeout; + return this; + } + + /** + *

+ * Enable/disable reduced logcat output + *

+ *

+ * Note that this is a global setting + *

+ * + * @param useMinimal true for reduced output, false for full output + * @return This Builder object for method chaining + */ + public Builder setMinimalLogging(boolean useMinimal) { + Debug.setLogTypeEnabled(Debug.LOG_COMMAND | Debug.LOG_OUTPUT, !useMinimal); + return this; + } + + /** + * Construct a {@link Interactive} instance, and start the shell + * + * @return Interactive shell + */ + public Interactive open() { + return new Interactive(this, null); + } + + /** + * Construct a {@link Interactive} instance, try to start the + * shell, and call onCommandResultListener to report success or failure + * + * @param onCommandResultListener Callback to return shell open status + * @return Interactive shell + */ + public Interactive open(OnCommandResultListener onCommandResultListener) { + return new Interactive(this, onCommandResultListener); + } + } + + /** + *

+ * An interactive shell - initially created with {@link Builder} - + * that executes blocks of commands you supply in the background, optionally + * calling callbacks as each block completes. + *

+ *

+ * STDERR output can be supplied as well, but due to compatibility with + * older Android versions, wantSTDERR is not implemented using + * redirectErrorStream, but rather appended to the output. STDOUT and STDERR + * are thus not guaranteed to be in the correct order in the output. + *

+ *

+ * Note as well that the close() and waitForIdle() methods will + * intentionally crash when run in debug mode from the main thread of the + * application. Any blocking call should be run from a background thread. + *

+ *

+ * When in debug mode, the code will also excessively log the commands + * passed to and the output returned from the shell. + *

+ *

+ * Though this function uses background threads to gobble STDOUT and STDERR + * so a deadlock does not occur if the shell produces massive output, the + * output is still stored in a List<String>, and as such doing + * something like 'ls -lR /' will probably have you run out of + * memory when using a {@link OnCommandResultListener}. A work-around + * is to not supply this callback, but using (only) + * {@link Builder#setOnSTDOUTLineListener(OnLineListener)}. This way, + * an internal buffer will not be created and wasting your memory. + *

+ *

Callbacks, threads and handlers

+ *

+ * On which thread the callbacks execute is dependent on your + * initialization. You can supply a custom Handler using + * {@link Builder#setHandler(Handler)} if needed. If you do not supply + * a custom Handler - unless you set + * {@link Builder#setAutoHandler(boolean)} to false - a Handler will + * be auto-created if the thread used for instantiation of the object has a + * Looper. + *

+ *

+ * If no Handler was supplied and it was also not auto-created, all + * callbacks will be called from either the STDOUT or STDERR gobbler + * threads. These are important threads that should be blocked as little as + * possible, as blocking them may in rare cases pause the native process or + * even create a deadlock. + *

+ *

+ * The main thread must certainly have a Looper, thus if you call + * {@link Builder#open()} from the main thread, a handler will (by + * default) be auto-created, and all the callbacks will be called on the + * main thread. While this is often convenient and easy to code with, you + * should be aware that if your callbacks are 'expensive' to execute, this + * may negatively impact UI performance. + *

+ *

+ * Background threads usually do not have a Looper, so calling + * {@link Builder#open()} from such a background thread will (by + * default) result in all the callbacks being executed in one of the gobbler + * threads. You will have to make sure the code you execute in these + * callbacks is thread-safe. + *

+ */ + public static class Interactive { + private final Handler handler; + private final boolean autoHandler; + private final String shell; + private final boolean wantSTDERR; + private final List commands; + private final Map environment; + private final OnLineListener onSTDOUTLineListener; + private final OnLineListener onSTDERRLineListener; + private int watchdogTimeout; + + private Process process = null; + private DataOutputStream STDIN = null; + private StreamGobbler STDOUT = null; + private StreamGobbler STDERR = null; + private ScheduledThreadPoolExecutor watchdog = null; + + private volatile boolean running = false; + private volatile boolean idle = true; // read/write only synchronized + private volatile boolean closed = true; + private volatile int callbacks = 0; + private volatile int watchdogCount; + + private final Object idleSync = new Object(); + private final Object callbackSync = new Object(); + + private volatile int lastExitCode = 0; + private volatile String lastMarkerSTDOUT = null; + private volatile String lastMarkerSTDERR = null; + private volatile Command command = null; + private volatile List buffer = null; + + /** + * The only way to create an instance: Shell.Builder::open() + * + * @param builder Builder class to take values from + */ + private Interactive(final Builder builder, + final OnCommandResultListener onCommandResultListener) { + autoHandler = builder.autoHandler; + shell = builder.shell; + wantSTDERR = builder.wantSTDERR; + commands = builder.commands; + environment = builder.environment; + onSTDOUTLineListener = builder.onSTDOUTLineListener; + onSTDERRLineListener = builder.onSTDERRLineListener; + watchdogTimeout = builder.watchdogTimeout; + + // If a looper is available, we offload the callbacks from the + // gobbling threads + // to whichever thread created us. Would normally do this in open(), + // but then we could not declare handler as final + if ((Looper.myLooper() != null) && (builder.handler == null) && autoHandler) { + handler = new Handler(); + } else { + handler = builder.handler; + } + + if (onCommandResultListener != null) { + // Allow up to 60 seconds for SuperSU/Superuser dialog, then enable + // the user-specified timeout for all subsequent operations + watchdogTimeout = 60; + commands.add(0, new Command(Shell.availableTestCommands, 0, new OnCommandResultListener() { + public void onCommandResult(int commandCode, int exitCode, List output) { + if ((exitCode == OnCommandResultListener.SHELL_RUNNING) && + !Shell.parseAvailableResult(output, SU.isSU(shell))) { + // shell is up, but it's brain-damaged + exitCode = OnCommandResultListener.SHELL_WRONG_UID; + } + watchdogTimeout = builder.watchdogTimeout; + onCommandResultListener.onCommandResult(0, exitCode, output); + } + }, null)); + } + + if (!open() && (onCommandResultListener != null)) { + onCommandResultListener.onCommandResult(0, + OnCommandResultListener.SHELL_EXEC_FAILED, null); + } + } + + @Override + protected void finalize() throws Throwable { + if (!closed && Debug.getSanityChecksEnabledEffective()) { + // waste of resources + Debug.log(ShellNotClosedException.EXCEPTION_NOT_CLOSED); + throw new ShellNotClosedException(); + } + super.finalize(); + } + + /** + * Add a command to execute + * + * @param command Command to execute + */ + public void addCommand(String command) { + addCommand(command, 0, (OnCommandResultListener) null); + } + + /** + *

+ * Add a command to execute, with a callback to be called on completion + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param command Command to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion + */ + public void addCommand(String command, int code, + OnCommandResultListener onCommandResultListener) { + addCommand(new String[]{ + command + }, code, onCommandResultListener); + } + + /** + *

+ * Add a command to execute, with a callback. This callback gobbles the + * output line by line without buffering it and also returns the result + * code on completion. + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param command Command to execute + * @param code User-defined value passed back to the callback + * @param onCommandLineListener Callback + */ + public void addCommand(String command, int code, OnCommandLineListener onCommandLineListener) { + addCommand(new String[]{ + command + }, code, onCommandLineListener); + } + + /** + * Add commands to execute + * + * @param commands Commands to execute + */ + public void addCommand(List commands) { + addCommand(commands, 0, (OnCommandResultListener) null); + } + + /** + *

+ * Add commands to execute, with a callback to be called on completion + * (of all commands) + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion + * (of all commands) + */ + public void addCommand(List commands, int code, + OnCommandResultListener onCommandResultListener) { + addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener); + } + + /** + *

+ * Add commands to execute, with a callback. This callback gobbles the + * output line by line without buffering it and also returns the result + * code on completion. + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandLineListener Callback + */ + public void addCommand(List commands, int code, + OnCommandLineListener onCommandLineListener) { + addCommand(commands.toArray(new String[commands.size()]), code, onCommandLineListener); + } + + /** + * Add commands to execute + * + * @param commands Commands to execute + */ + public void addCommand(String[] commands) { + addCommand(commands, 0, (OnCommandResultListener) null); + } + + /** + *

+ * Add commands to execute, with a callback to be called on completion + * (of all commands) + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandResultListener Callback to be called on completion + * (of all commands) + */ + public synchronized void addCommand(String[] commands, int code, + OnCommandResultListener onCommandResultListener) { + this.commands.add(new Command(commands, code, onCommandResultListener, null)); + runNextCommand(); + } + + /** + *

+ * Add commands to execute, with a callback. This callback gobbles the + * output line by line without buffering it and also returns the result + * code on completion. + *

+ *

+ * The thread on which the callback executes is dependent on various + * factors, see {@link Interactive} for further details + *

+ * + * @param commands Commands to execute + * @param code User-defined value passed back to the callback + * @param onCommandLineListener Callback + */ + public synchronized void addCommand(String[] commands, int code, + OnCommandLineListener onCommandLineListener) { + this.commands.add(new Command(commands, code, null, onCommandLineListener)); + runNextCommand(); + } + + /** + * Run the next command if any and if ready, signals idle state if no + * commands left + */ + private void runNextCommand() { + runNextCommand(true); + } + + /** + * Called from a ScheduledThreadPoolExecutor timer thread every second + * when there is an outstanding command + */ + private synchronized void handleWatchdog() { + final int exitCode; + + if (watchdog == null) + return; + if (watchdogTimeout == 0) + return; + + if (!isRunning()) { + exitCode = OnCommandResultListener.SHELL_DIED; + Debug.log(String.format("[%s%%] SHELL_DIED", shell.toUpperCase(Locale.ENGLISH))); + } else if (watchdogCount++ < watchdogTimeout) { + return; + } else { + exitCode = OnCommandResultListener.WATCHDOG_EXIT; + Debug.log(String.format("[%s%%] WATCHDOG_EXIT", shell.toUpperCase(Locale.ENGLISH))); + } + + postCallback(command, exitCode, buffer); + + // prevent multiple callbacks for the same command + command = null; + buffer = null; + idle = true; + + watchdog.shutdown(); + watchdog = null; + kill(); + } + + /** + * Start the periodic timer when a command is submitted + */ + private void startWatchdog() { + if (watchdogTimeout == 0) { + return; + } + watchdogCount = 0; + watchdog = new ScheduledThreadPoolExecutor(1); + watchdog.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + handleWatchdog(); + } + }, 1, 1, TimeUnit.SECONDS); + } + + /** + * Disable the watchdog timer upon command completion + */ + private void stopWatchdog() { + if (watchdog != null) { + watchdog.shutdownNow(); + watchdog = null; + } + } + + /** + * Run the next command if any and if ready + * + * @param notifyIdle signals idle state if no commands left ? + */ + private void runNextCommand(boolean notifyIdle) { + // must always be called from a synchronized method + + boolean running = isRunning(); + if (!running) + idle = true; + + if (running && idle && (commands.size() > 0)) { + Command command = commands.get(0); + commands.remove(0); + + buffer = null; + lastExitCode = 0; + lastMarkerSTDOUT = null; + lastMarkerSTDERR = null; + + if (command.commands.length > 0) { + try { + if (command.onCommandResultListener != null) { + // no reason to store the output if we don't have an + // OnCommandResultListener + // user should catch the output with an + // OnLineListener in this case + buffer = Collections.synchronizedList(new ArrayList()); + } + + idle = false; + this.command = command; + startWatchdog(); + for (String write : command.commands) { + Debug.logCommand(String.format("[%s+] %s", + shell.toUpperCase(Locale.ENGLISH), write)); + STDIN.write((write + "\n").getBytes("UTF-8")); + } + STDIN.write(("echo " + command.marker + " $?\n").getBytes("UTF-8")); + STDIN.write(("echo " + command.marker + " >&2\n").getBytes("UTF-8")); + STDIN.flush(); + } catch (IOException e) { + // STDIN might have closed + } + } else { + runNextCommand(false); + } + } else if (!running) { + // our shell died for unknown reasons - abort all submissions + while (commands.size() > 0) { + postCallback(commands.remove(0), OnCommandResultListener.SHELL_DIED, null); + } + } + + if (idle && notifyIdle) { + synchronized (idleSync) { + idleSync.notifyAll(); + } + } + } + + /** + * Processes a STDOUT/STDERR line containing an end/exitCode marker + */ + private synchronized void processMarker() { + if (command.marker.equals(lastMarkerSTDOUT) + && (command.marker.equals(lastMarkerSTDERR))) { + postCallback(command, lastExitCode, buffer); + stopWatchdog(); + command = null; + buffer = null; + idle = true; + runNextCommand(); + } + } + + /** + * Process a normal STDOUT/STDERR line + * + * @param line Line to process + * @param listener Callback to call or null + */ + private synchronized void processLine(String line, OnLineListener listener) { + if (listener != null) { + if (handler != null) { + final String fLine = line; + final OnLineListener fListener = listener; + + startCallback(); + handler.post(new Runnable() { + @Override + public void run() { + try { + fListener.onLine(fLine); + } finally { + endCallback(); + } + } + }); + } else { + listener.onLine(line); + } + } + } + + /** + * Add line to internal buffer + * + * @param line Line to add + */ + private synchronized void addBuffer(String line) { + if (buffer != null) { + buffer.add(line); + } + } + + /** + * Increase callback counter + */ + private void startCallback() { + synchronized (callbackSync) { + callbacks++; + } + } + + /** + * Schedule a callback to run on the appropriate thread + */ + private void postCallback(final Command fCommand, final int fExitCode, + final List fOutput) { + if (fCommand.onCommandResultListener == null && fCommand.onCommandLineListener == null) { + return; + } + if (handler == null) { + if (fCommand.onCommandResultListener != null) + fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, + fOutput); + if (fCommand.onCommandLineListener != null) + fCommand.onCommandLineListener.onCommandResult(fCommand.code, fExitCode); + return; + } + startCallback(); + handler.post(new Runnable() { + @Override + public void run() { + try { + if (fCommand.onCommandResultListener != null) + fCommand.onCommandResultListener.onCommandResult(fCommand.code, + fExitCode, fOutput); + if (fCommand.onCommandLineListener != null) + fCommand.onCommandLineListener + .onCommandResult(fCommand.code, fExitCode); + } finally { + endCallback(); + } + } + }); + } + + /** + * Decrease callback counter, signals callback complete state when + * dropped to 0 + */ + private void endCallback() { + synchronized (callbackSync) { + callbacks--; + if (callbacks == 0) { + callbackSync.notifyAll(); + } + } + } + + /** + * Internal call that launches the shell, starts gobbling, and starts + * executing commands. See {@link Interactive} + * + * @return Opened successfully ? + */ + private synchronized boolean open() { + Debug.log(String.format("[%s%%] START", shell.toUpperCase(Locale.ENGLISH))); + + try { + // setup our process, retrieve STDIN stream, and STDOUT/STDERR + // gobblers + if (environment.size() == 0) { + process = Runtime.getRuntime().exec(shell); + } else { + Map newEnvironment = new HashMap(); + newEnvironment.putAll(System.getenv()); + newEnvironment.putAll(environment); + int i = 0; + String[] env = new String[newEnvironment.size()]; + for (Map.Entry entry : newEnvironment.entrySet()) { + env[i] = entry.getKey() + "=" + entry.getValue(); + i++; + } + process = Runtime.getRuntime().exec(shell, env); + } + + STDIN = new DataOutputStream(process.getOutputStream()); + STDOUT = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "-", + process.getInputStream(), new OnLineListener() { + @Override + public void onLine(String line) { + synchronized (Interactive.this) { + if (command == null) { + return; + } + + String contentPart = line; + String markerPart = null; + + int markerIndex = line.indexOf(command.marker); + if (markerIndex == 0) { + contentPart = null; + markerPart = line; + } else if (markerIndex > 0) { + contentPart = line.substring(0, markerIndex); + markerPart = line.substring(markerIndex); + } + + if (contentPart != null) { + addBuffer(contentPart); + processLine(contentPart, onSTDOUTLineListener); + processLine(contentPart, command.onCommandLineListener); + } + + if (markerPart != null) { + try { + lastExitCode = Integer.valueOf( + markerPart.substring(command.marker.length() + 1), 10); + } catch (Exception e) { + // this really shouldn't happen + e.printStackTrace(); + } + lastMarkerSTDOUT = command.marker; + processMarker(); + } + } + } + }); + STDERR = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "*", + process.getErrorStream(), new OnLineListener() { + @Override + public void onLine(String line) { + synchronized (Interactive.this) { + if (command == null) { + return; + } + + String contentPart = line; + + int markerIndex = line.indexOf(command.marker); + if (markerIndex == 0) { + contentPart = null; + } else if (markerIndex > 0) { + contentPart = line.substring(0, markerIndex); + } + + if (contentPart != null) { + if (wantSTDERR) + addBuffer(contentPart); + processLine(contentPart, onSTDERRLineListener); + } + + if (markerIndex >= 0) { + lastMarkerSTDERR = command.marker; + processMarker(); + } + } + } + }); + + // start gobbling and write our commands to the shell + STDOUT.start(); + STDERR.start(); + + running = true; + closed = false; + + runNextCommand(); + + return true; + } catch (IOException e) { + // shell probably not found + return false; + } + } + + /** + * Close shell and clean up all resources. Call this when you are done + * with the shell. If the shell is not idle (all commands completed) you + * should not call this method from the main UI thread because it may + * block for a long time. This method will intentionally crash your app + * (if in debug mode) if you try to do this anyway. + */ + public void close() { + boolean _idle = isIdle(); // idle must be checked synchronized + + synchronized (this) { + if (!running) + return; + running = false; + closed = true; + } + + // This method should not be called from the main thread unless the + // shell is idle and can be cleaned up with (minimal) waiting. Only + // throw in debug mode. + if (!_idle && Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { + Debug.log(ShellOnMainThreadException.EXCEPTION_NOT_IDLE); + throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_NOT_IDLE); + } + + if (!_idle) + waitForIdle(); + + try { + try { + STDIN.write(("exit\n").getBytes("UTF-8")); + STDIN.flush(); + } catch (IOException e) { + if (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed")) { + // we're not running a shell, the shell closed STDIN, + // the script already contained the exit command, etc. + } else { + throw e; + } + } + + // wait for our process to finish, while we gobble away in the + // background + process.waitFor(); + + // make sure our threads are done gobbling, our streams are + // closed, and the process is destroyed - while the latter two + // shouldn't be needed in theory, and may even produce warnings, + // in "normal" Java they are required for guaranteed cleanup of + // resources, so lets be safe and do this on Android as well + try { + STDIN.close(); + } catch (IOException e) { + // STDIN going missing is no reason to abort + } + STDOUT.join(); + STDERR.join(); + stopWatchdog(); + process.destroy(); + } catch (IOException e) { + // various unforseen IO errors may still occur + } catch (InterruptedException e) { + // this should really be re-thrown + } + + Debug.log(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH))); + } + + /** + * Try to clean up as much as possible from a shell that's gotten itself + * wedged. Hopefully the StreamGobblers will croak on their own when the + * other side of the pipe is closed. + */ + public synchronized void kill() { + running = false; + closed = true; + + try { + STDIN.close(); + } catch (IOException e) { + // in case it was closed + } + try { + process.destroy(); + } catch (Exception e) { + // in case it was already destroyed or can't be + } + + idle = true; + synchronized (idleSync) { + idleSync.notifyAll(); + } + } + + /** + * Is our shell still running ? + * + * @return Shell running ? + */ + public boolean isRunning() { + if (process == null) { + return false; + } + try { + process.exitValue(); + return false; + } catch (IllegalThreadStateException e) { + // if this is thrown, we're still running + } + return true; + } + + /** + * Have all commands completed executing ? + * + * @return Shell idle ? + */ + public synchronized boolean isIdle() { + if (!isRunning()) { + idle = true; + synchronized (idleSync) { + idleSync.notifyAll(); + } + } + return idle; + } + + /** + *

+ * Wait for idle state. As this is a blocking call, you should not call + * it from the main UI thread. If you do so and debug mode is enabled, + * this method will intentionally crash your app. + *

+ *

+ * If not interrupted, this method will not return until all commands + * have finished executing. Note that this does not necessarily mean + * that all the callbacks have fired yet. + *

+ *

+ * If no Handler is used, all callbacks will have been executed when + * this method returns. If a Handler is used, and this method is called + * from a different thread than associated with the Handler's Looper, + * all callbacks will have been executed when this method returns as + * well. If however a Handler is used but this method is called from the + * same thread as associated with the Handler's Looper, there is no way + * to know. + *

+ *

+ * In practice this means that in most simple cases all callbacks will + * have completed when this method returns, but if you actually depend + * on this behavior, you should make certain this is indeed the case. + *

+ *

+ * See {@link Interactive} for further details on threading and + * handlers + *

+ * + * @return True if wait complete, false if wait interrupted + */ + public boolean waitForIdle() { + if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { + Debug.log(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); + throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE); + } + + if (isRunning()) { + synchronized (idleSync) { + while (!idle) { + try { + idleSync.wait(); + } catch (InterruptedException e) { + return false; + } + } + } + + if ((handler != null) && + (handler.getLooper() != null) && + (handler.getLooper() != Looper.myLooper())) { + // If the callbacks are posted to a different thread than + // this one, we can wait until all callbacks have called + // before returning. If we don't use a Handler at all, the + // callbacks are already called before we get here. If we do + // use a Handler but we use the same Looper, waiting here + // would actually block the callbacks from being called + + synchronized (callbackSync) { + while (callbacks > 0) { + try { + callbackSync.wait(); + } catch (InterruptedException e) { + return false; + } + } + } + } + } + + return true; + } + + /** + * Are we using a Handler to post callbacks ? + * + * @return Handler used ? + */ + public boolean hasHandler() { + return (handler != null); + } + } +} diff --git a/src/main/java/eu/chainfire/libsuperuser/ShellNotClosedException.java b/src/main/java/eu/chainfire/libsuperuser/ShellNotClosedException.java new file mode 100644 index 0000000..dba2b71 --- /dev/null +++ b/src/main/java/eu/chainfire/libsuperuser/ShellNotClosedException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012-2015 Jorrit "Chainfire" Jongma + * + * 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. + */ + +package eu.chainfire.libsuperuser; + +/** + * Exception class used to notify developer that a shell was not close()d + */ +@SuppressWarnings("serial") +public class ShellNotClosedException extends RuntimeException { + public static final String EXCEPTION_NOT_CLOSED = "Application did not close() interactive shell"; + + public ShellNotClosedException() { + super(EXCEPTION_NOT_CLOSED); + } +} diff --git a/src/main/java/eu/chainfire/libsuperuser/ShellOnMainThreadException.java b/src/main/java/eu/chainfire/libsuperuser/ShellOnMainThreadException.java new file mode 100644 index 0000000..41dcad6 --- /dev/null +++ b/src/main/java/eu/chainfire/libsuperuser/ShellOnMainThreadException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012-2015 Jorrit "Chainfire" Jongma + * + * 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. + */ + +package eu.chainfire.libsuperuser; + +/** + * Exception class used to crash application when shell commands are executed + * from the main thread, and we are in debug mode. + */ +@SuppressWarnings("serial") +public class ShellOnMainThreadException extends RuntimeException { + public static final String EXCEPTION_COMMAND = "Application attempted to run a shell command from the main thread"; + public static final String EXCEPTION_NOT_IDLE = "Application attempted to wait for a non-idle shell to close on the main thread"; + public static final String EXCEPTION_WAIT_IDLE = "Application attempted to wait for a shell to become idle on the main thread"; + public static final String EXCEPTION_TOOLBOX = "Application attempted to init the Toolbox class from the main thread"; + + public ShellOnMainThreadException(String message) { + super(message); + } +} diff --git a/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java b/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java new file mode 100644 index 0000000..55ac561 --- /dev/null +++ b/src/main/java/eu/chainfire/libsuperuser/StreamGobbler.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2012-2015 Jorrit "Chainfire" Jongma + * + * 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. + */ + +package eu.chainfire.libsuperuser; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; + +/** + * Thread utility class continuously reading from an InputStream + */ +public class StreamGobbler extends Thread { + /** + * Line callback interface + */ + public interface OnLineListener { + /** + *

Line callback

+ * + *

This callback should process the line as quickly as possible. + * Delays in this callback may pause the native process or even + * result in a deadlock

+ * + * @param line String that was gobbled + */ + void onLine(String line); + } + + private String shell = null; + private BufferedReader reader = null; + private List writer = null; + private OnLineListener listener = null; + + /** + *

StreamGobbler constructor

+ * + *

We use this class because shell STDOUT and STDERR should be read as quickly as + * possible to prevent a deadlock from occurring, or Process.waitFor() never + * returning (as the buffer is full, pausing the native process)

+ * + * @param shell Name of the shell + * @param inputStream InputStream to read from + * @param outputList {@literal List} to write to, or null + */ + public StreamGobbler(String shell, InputStream inputStream, List outputList) { + this.shell = shell; + reader = new BufferedReader(new InputStreamReader(inputStream)); + writer = outputList; + } + + /** + *

StreamGobbler constructor

+ * + *

We use this class because shell STDOUT and STDERR should be read as quickly as + * possible to prevent a deadlock from occurring, or Process.waitFor() never + * returning (as the buffer is full, pausing the native process)

+ * + * @param shell Name of the shell + * @param inputStream InputStream to read from + * @param onLineListener OnLineListener callback + */ + public StreamGobbler(String shell, InputStream inputStream, OnLineListener onLineListener) { + this.shell = shell; + reader = new BufferedReader(new InputStreamReader(inputStream)); + listener = onLineListener; + } + + @Override + public void run() { + // keep reading the InputStream until it ends (or an error occurs) + try { + String line; + while ((line = reader.readLine()) != null) { + Debug.logOutput(String.format("[%s] %s", shell, line)); + if (writer != null) writer.add(line); + if (listener != null) listener.onLine(line); + } + } catch (IOException e) { + // reader probably closed, expected exit condition + } + + // make sure our stream is closed and resources will be freed + try { + reader.close(); + } catch (IOException e) { + // read already closed + } + } +} diff --git a/src/main/java/eu/chainfire/libsuperuser/Toolbox.java b/src/main/java/eu/chainfire/libsuperuser/Toolbox.java new file mode 100644 index 0000000..d68628a --- /dev/null +++ b/src/main/java/eu/chainfire/libsuperuser/Toolbox.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2012-2015 Jorrit "Chainfire" Jongma + * + * 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. + */ + +package eu.chainfire.libsuperuser; + +import android.os.Build; + +import java.util.List; +import java.util.Locale; + +/** + * Utility class to decide between toolbox and toybox calls on M. + * Note that some calls (such as 'ls') are present in both, this + * class will favor toybox variants. + * + * This may not be what you want, as both syntax and output may + * differ between the variants. + * + * Very specific warning, the 'mount' included with toybox tends + * to segfault, at least on the first few 6.0 firmwares. + */ +public class Toolbox { + private static final int TOYBOX_SDK = 23; + + private static final Object synchronizer = new Object(); + private static volatile String toybox = null; + + /** + * Initialize. Asks toybox which commands it supports. Throws an exception if called from + * the main thread in debug mode. + */ + public static void init() { + // already inited ? + if (toybox != null) return; + + // toybox is M+ + if (Build.VERSION.SDK_INT < TOYBOX_SDK) { + toybox = ""; + } else { + if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) { + Debug.log(ShellOnMainThreadException.EXCEPTION_TOOLBOX); + throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_TOOLBOX); + } + + // ask toybox which commands it has, and store the info + synchronized (synchronizer) { + toybox = ""; + + List output = Shell.SH.run("toybox"); + if (output != null) { + toybox = " "; + for (String line : output) { + toybox = toybox + line.trim() + " "; + } + } + } + } + } + + /** + * Format a command string, deciding on toolbox or toybox for its execution + * + * If init() has not already been called, it is called for you, which may throw an exception + * if we're in the main thread. + * + * Example: + * Toolbox.command("chmod 0.0 %s", "/some/file/somewhere"); + * + * Output: + * < M: "toolbox chmod 0.0 /some/file/somewhere" + * M+ : "toybox chmod 0.0 /some/file/somewhere" + * + * @param format String to format. First word is the applet name. + * @param args Arguments passed to String.format + * @return Formatted String prefixed with either toolbox or toybox + */ + public static String command(String format, Object... args) { + if (Build.VERSION.SDK_INT < TOYBOX_SDK) { + return String.format(Locale.ENGLISH, "toolbox " + format, args); + } + + if (toybox == null) init(); + + format = format.trim(); + String applet; + int p = format.indexOf(' '); + if (p >= 0) { + applet = format.substring(0, p); + } else { + applet = format; + } + + if (toybox.contains(" " + applet + " ")) { + return String.format(Locale.ENGLISH, "toybox " + format, args); + } else { + return String.format(Locale.ENGLISH, "toolbox " + format, args); + } + } +}