diff --git a/.github/__release_template.md b/.github/__release_template.md index b60e162..e71691c 100644 --- a/.github/__release_template.md +++ b/.github/__release_template.md @@ -24,8 +24,4 @@ For debug builds, please download from [here](../../releases/last-debug-ci) inst OS release number is minimal required version to run the binary, binaries from the previous OS release(s) should work. -#### About Web builds - -- `noderawfs`: using NodeJS's host filesystem API (requires NodeJS to run, see the *WebAssembly*->*Emscripten* section in [README.md](README.md#emscripten) for more details), build with this tag is intended for running on desktop OS that is not yet supported by magiskboot_build - [Magisk]: https://github.com/topjohnwu/Magisk/releases diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ba86490..309331c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,6 +14,7 @@ on: - main paths-ignore: - '.github/workflows/release.yaml' + - '.github/workflows/deploy.yaml' - '.github/ISSUE_TEMPLATE/**' - 'patches-contrib/**' - 'LICENSE' @@ -23,6 +24,7 @@ on: pull_request: paths-ignore: - '.github/workflows/release.yaml' + - '.github/workflows/deploy.yaml' - '.github/__release_template.txt' - 'patches-contrib/**' - 'LICENSE' @@ -780,12 +782,24 @@ jobs: permissions: actions: write needs: [vars] + strategy: + matrix: + include: + - upload_prefix: "node" + ldflags: "-sENVIRONMENT=node -sEXIT_RUNTIME=1 -sNODERAWFS" + check: YES + + - upload_prefix: "web" + ldflags: "-sENVIRONMENT=web -sASYNCIFY" + keep_shell_file: YES + opt_size: YES + env: vcpkg_triplet: "wasm32-emscripten" - emsdk_ver: 3.1.37 + emsdk_ver: 3.1.34 emsdk_cache_dir: "emsdk-cache" rust_target: "wasm32-unknown-emscripten" - upload_suffix: "emscripten-wasm32-noderawfs-static" + upload_suffix: "emscripten-wasm32-static" steps: - name: Prepare (Host) @@ -802,7 +816,7 @@ jobs: key: emsdk-${{ env.emsdk_ver }}-${{ runner.os }} - name: Prepare (Emscripten) - uses: mymindstorm/setup-emsdk@v13 + uses: mymindstorm/setup-emsdk@v14 with: version: ${{ env.emsdk_ver }} actions-cache-folder: ${{ env.emsdk_cache_dir }} @@ -848,7 +862,7 @@ jobs: echo -e "\n### configure ###\n" . "$HOME/.cargo/env" - emcmake cmake -G Ninja -B build -DCMAKE_EXE_LINKER_FLAGS=" ${{ needs.vars.outputs.lto_ldflags }} -sNODERAWFS" \ + emcmake cmake -G Ninja -B build -DCMAKE_EXE_LINKER_FLAGS=" ${{ needs.vars.outputs.lto_ldflags }} ${{ matrix.ldflags }}" \ -DRust_CARGO_TARGET=${{ env.rust_target }} -DFULL_RUST_LTO=${{ needs.vars.outputs.full_lto }} \ -DCMAKE_BUILD_TYPE=${{ needs.vars.outputs.cmake_build_type }} -DPREFER_STATIC_LINKING=ON \ -DCMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake \ @@ -859,8 +873,17 @@ jobs: cmake --build build -j $(nproc) -v emstrip build/magiskboot*.wasm + if [[ "${{ matrix.keep_shell_file }}" != 'YES' ]]; then + rm -f build/magiskboot*.html + fi + file build/magiskboot* - $EMSDK_NODE ./build/magiskboot*.js || true + du -h build/magiskboot* + + if [[ "${{ matrix.check }}" == "YES" ]]; then + $EMSDK_NODE ./build/magiskboot*.js || true + fi + rm -rf $GITHUB_WORKSPACE/${{ env.progname }}-out && mkdir -p $GITHUB_WORKSPACE/${{ env.progname }}-out cp -afv build/magiskboot* $GITHUB_WORKSPACE/${{ env.progname }}-out @@ -868,10 +891,11 @@ jobs: uses: actions/upload-artifact@v4 with: if-no-files-found: error - name: ${{ env.progname }}-${{ needs.vars.outputs.short_sha }}-${{ needs.vars.outputs.build_type }}-${{ env.upload_suffix }} + name: ${{ env.progname }}-${{ needs.vars.outputs.short_sha }}-${{ needs.vars.outputs.build_type }}-${{ matrix.upload_prefix }}-${{ env.upload_suffix }} path: ${{ github.workspace }}/${{ env.progname }}-out/magiskboot* - name: Check + if: matrix.check == 'YES' run: | cd magiskboot_*-src/ EMULATOR=$EMSDK_NODE ./scripts/magiskboot_test.sh $GITHUB_WORKSPACE/${{ env.progname }}-out/magiskboot*.js diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..ce8d740 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,41 @@ +name: Deploy +on: + workflow_dispatch: + inputs: + ci_run_id: + description: "Run ID of the desired CI run to download artifacts from" + required: true + +env: + projname: "magiskboot-build-wasm" + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + steps: + - name: Download artifacts + id: download-artifact + uses: dawidd6/action-download-artifact@v3 + with: + workflow: ci.yaml + workflow_conclusion: success + run_id: ${{ github.event.inputs.ci_run_id }} + path: artifacts + skip_unpack: true + + - name: Extract artifacts + run: | + unzip -j -d dists artifacts/magiskboot-*-*-web-emscripten-wasm32-static.zip 'magiskboot*.*' + + - name: Rename files + run: | + cd dists + mv magiskboot*.html index.html + + - name: Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy dists --project-name=${{ env.projname }} diff --git a/CMakeLists.compat.txt b/CMakeLists.compat.txt index a61020a..372718a 100644 --- a/CMakeLists.compat.txt +++ b/CMakeLists.compat.txt @@ -45,6 +45,8 @@ elseif (WIN32) elseif (CYGWIN) add_compile_definitions(__USE_LINUX_IOCTL_DEFS) elseif (EMSCRIPTEN) + set(CMAKE_EXECUTABLE_SUFFIX ".html") + # exists in musl but not in rust libc crate list(APPEND RUSTFLAGS --cfg mbb_stubs_SYS_dup3) else() diff --git a/CMakeLists.stub.txt b/CMakeLists.stub.txt index 5fada59..19bc536 100644 --- a/CMakeLists.stub.txt +++ b/CMakeLists.stub.txt @@ -104,6 +104,23 @@ if (WIN32) endif() if (EMSCRIPTEN) + target_sources(magiskboot_exe PRIVATE + src/web-shims/conio_hack.cc) + set_source_files_properties(src/Magisk/native/src/boot/main.cpp PROPERTIES + COMPILE_FLAGS -Dmain=__mbb_main) + target_link_libraries(magiskboot_exe embind) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--wrap=printf -Wl,--wrap=iprintf -Wl,--wrap=write") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--wrap=fprintf -Wl,--wrap=fiprintf -Wl,--wrap=exit") + + message(STATUS "Using custom HTML wrapper") + + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -sFORCE_FILESYSTEM -sEXPORTED_RUNTIME_METHODS=ENV,FS,TTY,callMain,abort,mbb_get_wasm_name,mbb_set_this_prog") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --pre-js=${CMAKE_CURRENT_SOURCE_DIR}/src/web-shims/impl.pre.js") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --js-library=${CMAKE_CURRENT_SOURCE_DIR}/src/web-shims/utils.lib.js") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --shell-file=${CMAKE_CURRENT_SOURCE_DIR}/src/web-shims/shfile.in.html") + set_target_properties(magiskboot_exe PROPERTIES LINK_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/web-shims/impl.pre.js;${CMAKE_CURRENT_SOURCE_DIR}/src/web-shims/utils.lib.js;${CMAKE_CURRENT_SOURCE_DIR}/src/web-shims/shfile.in.html") + message(STATUS "Using Emscripten mmap stubs") target_sources(platform_stubs PRIVATE diff --git a/README.md b/README.md index 3330645..94ae7df 100644 --- a/README.md +++ b/README.md @@ -178,33 +178,27 @@ On arch-cygwin, set `CYGWIN_CC` and `CYGWIN_CXX` to `x86_64-pc-cygwin-clang` and -
WebAssembly +
Web #### Emscripten -> **Note** -> -> Currently this port only support running with NodeJS. -> -> A web frontend wrapper must be written to handle the argument passing and file system management in browsers. -> -> This should be implemented soon in the future ([#19](../../issues/19)), but unfortunately I am not a web developer, any help on this will be welcome QwQ - Please read the [Cross compiling](#cross-compiling) instructions first. -Install the [Emscripten][Emscripten] SDK and also a Rust compiler with Emscripten target (probably via [rustup][rustup]). +Install the [Emscripten SDK][emsdk] and also a Rust compiler with Emscripten target (probably via [rustup][rustup]). > **Warning** > -> emsdk version 3.1.37 is recommended, you might run into weird problems with other versions. +> An emsdk with version between 3.1.31 and 3.1.34 is required, this project is known to not build or work correctly with other SDK versions. Use [vcpkg][vcpkg] to install the [depended libraries](#requirements), the triplet is called `wasm32-emscripten`. -When configuring, use `emcmake cmake` instead of `cmake` (but don't use it for `cmake --build` and other CMake commands) , and use `/path/to/your/emsdk/emscripten/cmake/Modules/Platform/Emscripten.cmake` as the toolchain file for vcpkg. +When configuring, use `emcmake cmake` instead of `cmake` (but don't use it for `cmake --build` and other CMake commands) , and use `/path/to/your/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake` as the toolchain file for vcpkg. + +To target web browsers, set `-sENVIRONMENT=web` in `CMAKE_EXE_LINKER_FLAGS` (recommended step, for reducing JS size). If you want real-time console output, set `-sASYNCIFY` as well (Note this will increase the binary size significantly). -For NodeJS, make sure to set `CMAKE_EXE_LINKER_FLAGS` to `-sNODERAWFS` to allow using the host filesystem. +For NodeJS, set `-sENVIRONMENT=node` instead. **Make sure** to also set `-sEXIT_RUNTIME=1` and `-sNODERAWFS`. And then you will be able to run the end result with [NodeJS][NodeJS] like this: `node magiskboot.js` -finally, you can run the result with [NodeJS][NodeJS] using: `node magiskboot.js` +You can find more details about these linker flags in [Emscripten documentation](https://emscripten.org/docs/tools_reference/settings_reference.html).
@@ -409,6 +403,6 @@ For more details about these licenses, please see [LICENSE](LICENSE) and [LICENS [fedora-cygwin]: https://copr.fedorainfracloud.org/coprs/yselkowitz/cygwin/ [cmake-toolchains]: https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html [vcpkg]: https://vcpkg.io/ -[Emscripten]: https://github.com/emscripten-core/emsdk +[emsdk]: https://github.com/emscripten-core/emsdk [NodeJS]: https://nodejs.org/ [ONDK]: https://github.com/topjohnwu/ondk diff --git a/src/web-shims/conio_hack.cc b/src/web-shims/conio_hack.cc new file mode 100644 index 0000000..20abcc2 --- /dev/null +++ b/src/web-shims/conio_hack.cc @@ -0,0 +1,213 @@ +#include +#include +#include +#include +#include + +#include +#include + +/* 50 lines */ +#define MBB_CONIO_BASE_THRESHOLD (50) + +/* 20 ms */ +#define MBB_CONIO_BASE_DELAY (20) + +/* 1 ms */ +#define MBB_CONIO_LOWEST_DELAY (1) + +static char mbb_conio_cnt; + +static void (*conout_hook_ptr)(const char *s, size_t len); + +// used for exit code callback in JS + +extern int __mbb_main(int argc, char **argv); + +// defined in src/Magisk/native/src/boot/main.cpp +// renamed by our -Dmain=__mbb_main in CMakeLists.stubs.txt +static int (*main_ptr)(int argc, char **argv) = __mbb_main; + +// used for handling exits in Rust code + +__attribute__((noreturn)) extern "C" void __real_exit(int status); + +__attribute__((noreturn)) static void (*exit_ptr)(int status) = __real_exit; + +// used for intercepting writes in Rust code + +extern "C" ssize_t __real_write(int fd, const void *buf, size_t count); + +ssize_t (*write_ptr)(int fd, const void *buf, size_t count) = __real_write; + +// used for intercepting writes in C++ code + +static int (*vprintf_ptr)(const char *format, va_list ap) = vprintf; + +static int (*vfprintf_ptr)(FILE *stream, const char *format, va_list ap) = vfprintf; + +// redirect these overrided calls to our ptrs + +extern "C" { + __attribute__((noreturn)) void __wrap_exit(int status) { + exit_ptr(status); + } + + ssize_t __wrap_write(int fd, const void *buf, size_t count) { + return write_ptr(fd, buf, count); + } + + int __wrap_printf(const char *format, ...) { + va_list ap; + int res; + + va_start(ap, format); + res = vprintf_ptr(format, ap); + va_end(ap); + + return res; + } + + int __wrap_iprintf(const char *format, ...) { + va_list ap; + int res; + + va_start(ap, format); + res = vprintf_ptr(format, ap); + va_end(ap); + + return res; + } + + int __wrap_fprintf(FILE *stream, const char *format, ...) { + va_list ap; + int res; + + va_start(ap, format); + res = vfprintf_ptr(stream, format, ap); + va_end(ap); + + return res; + } + + int __wrap_fiprintf(FILE *stream, const char *format, ...) { + va_list ap; + int res; + + va_start(ap, format); + res = vfprintf_ptr(stream, format, ap); + va_end(ap); + + return res; + } +} + +static inline void __exit_hook(int status) { + EM_ASM({ + Module['mbb_main_cb']($0); + }, status); +} + +static inline bool __should_sleep(const char *s, size_t len) { + + return !!memchr(s, '\n', len); +} + +static void __conout_hook_fast(const char *s, size_t len) { + if (!__should_sleep(s, len)) + return; + + emscripten_sleep(MBB_CONIO_LOWEST_DELAY); +} + +static void __conout_hook_dflt(const char *s, size_t len) { + if (!__should_sleep(s, len)) + return; + + if (++mbb_conio_cnt >= MBB_CONIO_BASE_THRESHOLD) { + mbb_conio_cnt = 0; + conout_hook_ptr = __conout_hook_fast; + return; + } + + emscripten_sleep(MBB_CONIO_BASE_DELAY); +} + +static int __main_wrap(int argc, char **argv) { + int res; + + // reset conout hook state + conout_hook_ptr = __conout_hook_dflt; + mbb_conio_cnt = 0; + + res = __mbb_main(argc, argv); + + // asyncify breaks retval, so we do this to inform js side + __exit_hook(res); + + return 0; +} + +__attribute__((noreturn)) static void __exit_wrap(int status) { + __exit_hook(status); + __real_exit(status); +} + +static inline void __check_do_conout_hook(int fd, const char *s, size_t len) { + switch (fd) { + case STDOUT_FILENO: + case STDERR_FILENO: + conout_hook_ptr(s, len); + break; + default: + break; + } +} + +ssize_t __write_wrap(int fd, const void *buf, size_t count) { + ssize_t res; + + res = __real_write(fd, buf, count); + if (!(res > 0)) + return res; + + __check_do_conout_hook(fd, (const char *) buf, count); + + return res; +} + +static int __vprintf_wrap(const char *format, va_list ap) { + int res; + + res = vprintf(format, ap); + conout_hook_ptr(format, strlen(format)); + + return res; +} + +static int __vfprintf_wrap(FILE *stream, const char *format, va_list ap) { + int res; + + res = vfprintf(stream, format, ap); + __check_do_conout_hook(fileno(stream), format, strlen(format)); + + return res; +} + +int main(int argc, char **argv) { + return main_ptr(argc, argv); +} + +static void __enable_conio_hack(void) { + main_ptr = __main_wrap; + + exit_ptr = __exit_wrap; + + write_ptr = __write_wrap; + vprintf_ptr = __vprintf_wrap; + vfprintf_ptr = __vfprintf_wrap; +} + +EMSCRIPTEN_BINDINGS(conio_hack) { + emscripten::function("mbb_enable_conio_hack", &__enable_conio_hack); +} diff --git a/src/web-shims/impl.pre.js b/src/web-shims/impl.pre.js new file mode 100644 index 0000000..fefb581 --- /dev/null +++ b/src/web-shims/impl.pre.js @@ -0,0 +1,470 @@ +var Module = { +#if NODERAWFS + // only for targeting NodeJS + + 'preInit': () => { + // temporary solution for inheriting system environ: + // https://github.com/emscripten-core/emscripten/pull/3948#issuecomment-744032264 + Object.assign(Module.ENV, process.env); + }, +#else + // only for targeting browsers + + 'noInitialRun': true, // prevent calling main() on page load + 'instantiateWasm': (imps, cb) => { + if (Module.mbb_get_wasm_name === undefined) { + // library function not yet initialized, maybe later + + setTimeout(() => { + Module['instantiateWasm'](imps, cb); + }, 50); + + return; + } + + const status_label = document.getElementById('status_label').childNodes[0]; + const status_show = document.getElementById('status_show'); + + status_label.textContent = 'Fetching: '; + status_show.textContent = '-- % (? / ?)'; + + // fetch wasm + + const xhr = new XMLHttpRequest(); + + xhr.open('GET', Module.mbb_get_wasm_name(), true); + xhr.responseType = 'arraybuffer'; + xhr.onprogress = (ev) => { + if (ev.lengthComputable) { + const pct = Math.floor(100 * (ev.loaded / ev.total)); + status_show.textContent = `${pct} % (${ev.loaded} / ${ev.total})`; + } else + status_show.textContent = `-- % (${ev.loaded} / ?)`; + }; + xhr.onload = async () => { + var succeed = false; + var wasm = null; + + if (xhr.status === 200) { + status_label.textContent = 'Compiling: '; + status_show.textContent = 'Take longer on slow devices'; + + try { + wasm = await WebAssembly.instantiate(xhr.response, imps); + succeed = true; + } catch (exc) { + status_label.textContent = 'WebAssembly Error: '; + status_show.textContent = exc.message; + } + } else { + status_label.textContent = 'HTTP Error: '; + status_show.textContent = xhr.statusText; + } + + if (succeed) + cb(wasm.instance); + else + Module.abort(); // or emscripten will wait forever + }; + xhr.onerror = (_) => { + status_label.textContent = 'Network Error'; + status_show.textContent = ''; + + Module.abort(); // or emscripten will wait forever + }; + xhr.onabort = (_) => { + status_label.textContent = 'Network Error: '; + status_show.textContent = 'Connection is aborted'; + + Module.abort(); // or emscripten will wait forever + }; + xhr.ontimeout = (_) => { + status_label.textContent = 'Network Error: '; + status_show.textContent = 'Request timeout'; + + Module.abort(); // or emscripten will wait forever + }; + xhr.send(); + + return {}; + }, + 'preInit': () => { + // set a pretty argv[0] + Module.mbb_set_this_prog(Module.mbb_get_wasm_name().split('.wasm')[0]); + + // bind stdout + + const conout = document.getElementById('conout'); + const _dec = new TextDecoder(); + Module.TTY.stream_ops.write = (_, buff, off, len) => { + const arr = buff.subarray(off, off + len); + conout.value += _dec.decode(arr); + conout.scrollTop = conout.scrollHeight; + + return len; + } + + // filesystem + + const cwd_show = document.getElementById('cwd_show'); + const fileop_panel = document.getElementById('fileop_panel'); + const dirent_tab = document.getElementById('dirent_tab'); + + function mbb_do_cwd_disp() { + cwd_show.textContent = Module.FS.cwd(); + } + + const cp_btn = document.getElementById('cp_btn'); + cp_btn.addEventListener('click', () => { + const ent = dirent_tab.querySelector('.mbb_highlight'); + + if (ent === null) + return; + + const name = ent.textContent; + var new_name = prompt("New name for the copied file:", name); + + if (new_name === null) + return; + + new_name = new_name.trim(); + if (new_name.length === 0 + || new_name === name) + return; + + const data = Module.FS.readFile(name); + mbb_fs_err_ignored(() => { + Module.FS.writeFile(new_name, data); + mbb_do_dirent_disp(); + }); + }); + + function mbb_do_dirent_disp() { + fileop_panel.disabled = true; + cp_btn.disabled = true; + dirent_tab.innerHTML = ''; // remove old entries + Module.FS.readdir('.').sort().forEach((ent) => { + if (ent === '.') + return; // hide current dir + + var ent_name = ent; + + const buf = Module.FS.lstat(ent); + if (Module.FS.isDir(buf.mode)) { + ent_name = ent + '/'; + } + + dirent_tab.insertRow().insertCell().textContent = ent_name; + }); + } + + dirent_tab.addEventListener('click', (ev) => { + if (ev.target.tagName === 'TD') { + ev.preventDefault(); + + const prev_ent = dirent_tab.querySelector('.mbb_highlight'); + if (prev_ent !== null) { + if (prev_ent === ev.target) { + const name = ev.target.textContent; + const buf = Module.FS.lstat(name); + + if (Module.FS.isDir(buf.mode)) { + Module.FS.chdir(name); + mbb_do_cwd_disp(); + mbb_do_dirent_disp(); + } else { + const data = Module.FS.readFile(name); + const blob = new Blob([data]); + const data_url = window.URL.createObjectURL(blob); + + const dl_link = document.createElement('a'); + dl_link.download = name; + dl_link.href = data_url; + dl_link.click(); + + setTimeout(() => { + window.URL.revokeObjectURL(data_url); + }, 0); + } + + return; + } + + prev_ent.classList.remove('mbb_highlight'); + } + + ev.target.classList.add('mbb_highlight'); + fileop_panel.disabled = false; + + const name = ev.target.textContent; + const buf = Module.FS.lstat(name); + + if (Module.FS.isFile(buf.mode)) + cp_btn.disabled = false; + } + }); + + function mbb_fs_err_ignored(fn) { + try { + return fn(); + } catch (exc) { + if (exc instanceof Module.FS.ErrnoError) + return; + + throw exc; + } + } + + const mkdir_btn = document.getElementById('mkdir_btn'); + mkdir_btn.addEventListener('click', () => { + var name = prompt('Name for the new folder:'); + + if (name === null) + return; + + name = name.trim(); + if (name.length === 0) + return; + + mbb_fs_err_ignored(() => { + Module.FS.mkdir(name); + mbb_do_dirent_disp(); + }); + }); + + const imp_btn = document.getElementById('imp_btn'); + imp_btn.addEventListener('click', () => { + const file_picker = document.createElement('input'); + file_picker.type = 'file'; + file_picker.onchange = (ev) => { + const f = ev.target.files[0]; + + const rder = new FileReader(); + rder.onload = (_) => { + var name = prompt('Name for the imported file:', f.name); + + if (name === null) + return; + + name = name.trim(); + if (name.length === 0) + return; + + const data = new Uint8Array(rder.result); + + mbb_fs_err_ignored(() => { + Module.FS.writeFile(name, data); + mbb_do_dirent_disp(); + }); + }; + rder.readAsArrayBuffer(f); + }; + file_picker.click(); + }); + + const rm_btn = document.getElementById('rm_btn'); + rm_btn.addEventListener('click', () => { + const ent = dirent_tab.querySelector('.mbb_highlight'); + + if (ent === null) + return; + + const name = ent.textContent; + const buf = FS.lstat(name); + + if (Module.FS.isDir(buf.mode)) + mbb_fs_err_ignored(() => { + try { + Module.FS.rmdir(name); + } catch (exc) { + if (exc instanceof Module.FS.ErrnoError + && exc.errno === 55) { // ENOTEMPTY + alert('Directory is not empty'); + return; + } else + throw exc; + } + + mbb_do_dirent_disp(); + }); + else + mbb_fs_err_ignored(() => { + Module.FS.unlink(name); + mbb_do_dirent_disp(); + }); + }); + + const rename_btn = document.getElementById('rename_btn'); + rename_btn.addEventListener('click', () => { + const ent = dirent_tab.querySelector('.mbb_highlight'); + + if (ent === null) + return; + + var name = ent.textContent; + if (name.endsWith('/')) + name = name.slice(0, -1); + + var new_name = prompt('New name for this entry:', name); + + if (new_name === null) + return; + + new_name = new_name.trim(); + + if (new_name.length === 0 + || new_name === name) + return; + + mbb_fs_err_ignored(() => { + Module.FS.rename(name, new_name); + mbb_do_dirent_disp(); + }); + }); + + // start up + + const scr_sel = document.getElementById('scr_sel'); // always show TTY screen on load + window.location.href = '#scr_tty'; + + const status_label = document.getElementById('status_label').childNodes[0]; + const status_show = document.getElementById('status_show'); + window.onerror = (_) => { + status_show.textContent = 'Crashed'; + }; + + const cmdline_edit = document.getElementById('cmdline_edit'); + var status_upd = null; + Module.onRuntimeInitialized = () => { +#if ASYNCIFY + // turn on Web exclusive hacks + Module.mbb_enable_conio_hack(); +#else + alert('Asyncify is not enabled, real-time terminal output will not be available.\n\n' + + 'See README for more details.'); +#endif + + // set initial cwd to a nice place + Module.FS.chdir('/home/web_user'); + mbb_do_cwd_disp(); + mbb_do_dirent_disp(); + + // we can do stuffs now + + scr_sel.disabled = false; + + status_label.textContent = 'Status: '; + status_show.textContent = 'Idle'; + + cmdline_edit.readOnly = false; + }; + + // exec ctrl + + cmdline_edit.addEventListener('keydown', (ev) => { + if (ev.key === "Enter") { + if (cmdline_edit.readOnly) + return; // not safe to call main() yet + + ev.preventDefault(); + + if (status_upd !== null) { + clearTimeout(status_upd); + status_upd = null; + } + + // handle quoted arguments + + var args = cmdline_edit.value.match(/'[^']+'|"[^"]+"|\S+/g); + + if (args === null) + args = []; + + args = args.map((m) => { + if ((m.startsWith('\'') && m.endsWith('\'')) + || (m.startsWith('"') && m.endsWith('"'))) + return m.substring(1, m.length - 1); + else + return m; + }); + + cmdline_edit.readOnly = true; + scr_sel.disabled = true; + + cmdline_edit.value = ''; + conout.value = ''; // clear old output + status_show.textContent = 'Running'; + + Module.callMain(args); + } + }); + Module['mbb_main_cb'] = (ex) => { + status_upd = setTimeout(() => { + mbb_do_dirent_disp(); + status_show.textContent = `Exited (code ${ex})`; + + cmdline_edit.readOnly = false; + scr_sel.disabled = false; + }, 150); + }; + + // env vars + + const env_edit = document.getElementById('env_edit'); + const json = localStorage.getItem('mbb_environ'); + if (json !== null) { + // restore saved environ + + const environ = JSON.parse(json); + Object.keys(environ).forEach((k) => { + const v = environ[k]; + + Module.ENV[k] = v; // pass to emscripten + env_edit.value += `${k}=${v}\n`; + }); + env_edit.scrollTop = env_edit.scrollHeight; + } + + const clear_env_btn = document.getElementById('clear_env_btn'); + clear_env_btn.addEventListener('click', () => { + env_edit.value = ''; + }); + + const apply_env_btn = document.getElementById('apply_env_btn'); + apply_env_btn.addEventListener('click', () => { + const new_environ = {}; + + env_edit.value.split('\n').forEach((l) => { + const l_ = l.trim(); + + if (l_.length === 0) + return; // empty line + + const sep = l_.indexOf('='); + var k = null; + var v = null; + + if (sep === -1) { + // key only, flag vars? + k = l_; + v = ''; + } else { + k = l_.slice(0, sep); + v = l_.slice(sep + 1) + } + + new_environ[k] = v; + }); + + const new_json = JSON.stringify(new_environ); + localStorage.setItem('mbb_environ', new_json); + + document.body.style.display = 'none'; // hide page when reloading + + // emscripten limitation: + // setting ENV is only effective before runtime is initialized + window.location.reload(); + }); + }, +#endif +}; diff --git a/src/web-shims/shfile.in.html b/src/web-shims/shfile.in.html new file mode 100644 index 0000000..5e810f5 --- /dev/null +++ b/src/web-shims/shfile.in.html @@ -0,0 +1,167 @@ + + + + + + + magiskboot.wasm + + + + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + + +
+ +
+ + + + +
+
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+ + ⚬ Tap to select, tap again on the highlighted entry to navigate / export file +
+ +
+ +
+
+
+ +
+ + +
+
+ +
+ +
+
+
+ + +
+ + + {{{ SCRIPT }}} + + diff --git a/src/web-shims/utils.lib.js b/src/web-shims/utils.lib.js new file mode 100644 index 0000000..8c53c87 --- /dev/null +++ b/src/web-shims/utils.lib.js @@ -0,0 +1,18 @@ +#if !NODERAWFS +// for Web only + +// XXX: `mergeInto' is emsdk version dependent! +// check emscripten source to adapt to other vers +mergeInto(LibraryManager.library, { + // note: + // must use this full syntax to declare functions, + // otherwise emscripten will ignore them + $mbb_get_wasm_name: function() { + return wasmBinaryFile.split('/').pop(); + }, + + $mbb_set_this_prog: function(this_prog) { + thisProgram = this_prog; + }, +}); +#endif