From 37227ad7f183456ebd9f5055d01b1584d543e32e Mon Sep 17 00:00:00 2001 From: Fox2Code Date: Sat, 14 May 2022 23:47:43 +0200 Subject: [PATCH] Major installer rework + rework AnyKernel3 install system. --- .../assets/module_installer_anykernel3.sh | 82 +++++++++++ .../mmm/androidacy/AndroidacyRepoData.java | 7 +- .../mmm/installer/InstallerActivity.java | 133 +++++++++++------- .../mmm/installer/InstallerInitializer.java | 20 +-- .../java/com/fox2code/mmm/repo/RepoData.java | 5 +- .../com/fox2code/mmm/repo/RepoUpdater.java | 7 +- .../com/fox2code/mmm/utils/PropUtils.java | 3 + 7 files changed, 182 insertions(+), 75 deletions(-) create mode 100644 app/src/main/assets/module_installer_anykernel3.sh diff --git a/app/src/main/assets/module_installer_anykernel3.sh b/app/src/main/assets/module_installer_anykernel3.sh new file mode 100644 index 0000000..359024e --- /dev/null +++ b/app/src/main/assets/module_installer_anykernel3.sh @@ -0,0 +1,82 @@ +## AnyKernel3 (AK3), and AnyKernel2/AnyKernel 2.0 (AK2) Scripts License: +# +# AnyKernel (versions 2.0/2 and later) Android image modifying scripts. +# Copyright (c) 2019 Chris Renshaw (osm0sis @ xda-developers), +# and additional contributors per readily available commit history/credits. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted (subject to the limitations in the disclaimer +# below) provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +if [ -z "$AK3TMPFS" ]; then + echo "AK3TMPFS is not defined? Are you running FoxMMM?" + exit 1 +fi + +if [ ! -e "$AK3TMPFS" ]; then + mkdir $AK3TMPFS + chmod 755 $AK3TMPFS +fi + +ZIPFILE=$3; + +# Mount tmpfs early +mount -t tmpfs -o size=400M,noatime tmpfs $AK3TMPFS; +mount | grep -q " $AK3TMPFS " || exit 1; + +unzip -p $Z tools*/busybox > $AK3TMPFS/busybox; +unzip -p $Z META-INF/com/google/android/update-binary > $AK3TMPFS/update-binary; +## + +chmod 755 $AK3TMPFS/busybox; +$AK3TMPFS/busybox chmod 755 $AK3TMPFS/update-binary; +$AK3TMPFS/busybox chown root:root $AK3TMPFS/busybox $AK3TMPFS/update-binary; + +# work around Android passing the app what is actually a non-absolute path +AK3TMPFS=$($AK3TMPFS/busybox readlink -f $AK3TMPFS); + +# AK3 allows the zip to be flashed from anywhere so avoids any need to remount / +if $AK3TMPFS/busybox grep -q AnyKernel3 $AK3TMPFS/update-binary; then + # work around more restrictive upstream SELinux policies for Magisk <19306 + magiskpolicy --live "allow kernel app_data_file file write" || true; + TMP=$AK3TMPFS/tmp; +else + echo "Module is not an AnyKernel3 module!" + exit 1 +fi; + +# update-binary +ASH_STANDALONE=1 AKHOME=$AK3TMPFS/anykernel $AK3TMPFS/busybox ash $AK3TMPFS/update-binary 3 1 "$Z"; +RC=$?; + +# Original script delete all generated files, +# But we just need to unmount as we store everything inside tmpfs +umount $TMP; + +return $RC; \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java index 54d6204..b2989bd 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java @@ -70,7 +70,7 @@ public class AndroidacyRepoData extends RepoData { // Implementation details discussed on telegram long time = System.currentTimeMillis(); if (this.androidacyBlockade > time) return false; - this.androidacyBlockade = time + 5_000L; + this.androidacyBlockade = time + 30_000L; String cookies = AndroidacyRepoData.getCookies(); int start = cookies == null ? -1 : cookies.indexOf("USER="); String token = null; @@ -150,8 +150,9 @@ public class AndroidacyRepoData extends RepoData { for (int i = 0; i < len; i++) { jsonObject = jsonArray.getJSONObject(i); String moduleId = jsonObject.getString("codename"); - // Deny remote modules ids shorter than 3 chars - if (moduleId.length() < 3) continue; + // Deny remote modules ids shorter than 3 chars or containing null char or space + if (moduleId.length() < 3 || moduleId.indexOf('\0') != -1 || + moduleId.indexOf(' ') != -1 || "ak3-helper".equals(moduleId)) continue; long lastUpdate = jsonObject.getLong("updated_at") * 1000; lastLastUpdate = Math.max(lastLastUpdate, lastUpdate); RepoModule repoModule = this.moduleHashMap.get(moduleId); diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java index 17572ec..a63b6db 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java @@ -44,7 +44,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.PrintStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; @@ -168,31 +167,31 @@ public class InstallerActivity extends CompatActivity { Files.fixJavaZipHax(rawModule); boolean noPatch = false; boolean isModule = false; - boolean isAnyKernel = false; + boolean isAnyKernel3 = false; errMessage = "File is not a valid zip file"; try (ZipInputStream zipInputStream = new ZipInputStream( new ByteArrayInputStream(rawModule))) { ZipEntry zipEntry; while ((zipEntry = zipInputStream.getNextEntry()) != null) { String entryName = zipEntry.getName(); - if (entryName.equals("anykernel.sh")) { + if (entryName.equals("tools/ak3-core.sh")) { noPatch = true; - isAnyKernel = true; + isAnyKernel3 = true; break; } else if (entryName.equals("module.prop")) { noPatch = true; isModule = true; break; - } else if (entryName.endsWith("/anykernel.sh")) { - isAnyKernel = true; + } else if (entryName.endsWith("/tools/ak3-core.sh")) { + isAnyKernel3 = true; } else if (entryName.endsWith("/module.prop")) { isModule = true; } } } - if (!isModule && !isAnyKernel) { + if (!isModule && !isAnyKernel3) { this.setInstallStateFinished(false, - "! File is not a valid magisk module", ""); + "! File is not a valid Magisk or AnyKernel3 module", ""); return; } if (noPatch) { @@ -254,36 +253,37 @@ public class InstallerActivity extends CompatActivity { installJob = Shell.cmd("export MMM_EXT_SUPPORT=1", "cd \"" + this.moduleCache.getAbsolutePath() + "\"", "sh \"" + installScript.getAbsolutePath() + "\"" + - " /dev/null 1 \"" + file.getAbsolutePath() + "\"") + " 3 0 \"" + file.getAbsolutePath() + "\"") .to(installerController, installerMonitor); } else { String arch32 = "true"; // Do nothing by default boolean needs32bit = false; String moduleId = null; - boolean anyKernel = false; + boolean anyKernel3 = false; boolean magiskModule = false; - File anyKernelInstallScript = new File(this.moduleCache, "update-binary"); + String MAGISK_PATH = InstallerInitializer.peekMagiskPath(); + if (MAGISK_PATH == null) { + this.setInstallStateFinished(false, "! Unable to resolve magisk path", ""); + return; + } + String ASH = MAGISK_PATH + "/.magisk/busybox/busybox ash"; try (ZipFile zipFile = new ZipFile(file)) { // Check if module is AnyKernel module - if (zipFile.getEntry("anykernel.sh") != null) { + if (zipFile.getEntry("tools/ak3-core.sh") != null) { ZipEntry updateBinary = zipFile.getEntry( "META-INF/com/google/android/update-binary"); if (updateBinary != null) { BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(zipFile.getInputStream(updateBinary))); - PrintStream printStream = new PrintStream( - new FileOutputStream(anyKernelInstallScript)); String line; while ((line = bufferedReader.readLine()) != null) { - line = line.replace("/sbin/sh", "/system/bin/sh"); - line = line.replace("/data/adb/modules/ak3-helper", - "/data/adb/modules-update/ak3-helper"); - printStream.println(line); + if (line.contains("AnyKernel3")) { + anyKernel3 = true; + break; + } } - printStream.close(); bufferedReader.close(); } - anyKernel = true; } if (zipFile.getEntry( // Check if module hard require 32bit support "common/addon/Volume-Key-Selector/tools/arm64/keycheck") == null && @@ -310,13 +310,15 @@ public class InstallerActivity extends CompatActivity { null); return; } - if (magiskModule && moduleId == null && !anyKernel) { + if (magiskModule && moduleId == null && !anyKernel3) { // Modules without module Ids are module installed by 3rd party software this.setInstallStateFinished(false, "! Magisk modules require a moduleId", null); return; } - if (Build.SUPPORTED_32_BIT_ABIS.length == 0) { + if (anyKernel3) { + installerController.useRecoveryExt(); + } else if (Build.SUPPORTED_32_BIT_ABIS.length == 0) { if (needs32bit) { this.setInstallStateFinished(false, "! This module can't be installed on a 64bit only system", @@ -332,47 +334,45 @@ public class InstallerActivity extends CompatActivity { } String installCommand; File installExecutable; - if (anyKernel && moduleId == null) { // AnyKernel modules don't have a moduleId + if (anyKernel3 && moduleId == null) { // AnyKernel modules don't have a moduleId this.warnReboot = true; // We should probably re-flash magisk... - installExecutable = anyKernelInstallScript; + installExecutable = this.extractInstallScript("module_installer_anykernel3.sh"); + if (installExecutable == null) { + this.setInstallStateFinished(false, + "! Failed to extract AnyKernel3 module install script", null); + return; + } // "unshare -m" is needed to force mount namespace isolation. // This allow AnyKernel to mess-up with mounts point without crashing the system! - installCommand = "unshare -m sh \"" + installExecutable.getAbsolutePath() + "\"" + - " /dev/null 1 \"" + file.getAbsolutePath() + "\""; + installCommand = "unshare -m " + ASH + " \"" + + installExecutable.getAbsolutePath() + "\"" + + " 3 1 \"" + file.getAbsolutePath() + "\""; } else if (InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND && ((compatFlags & AppUpdateManager.FLAG_COMPAT_MAGISK_CMD) != 0 || noExtensions || MainApplication.isUsingMagiskCommand())) { installCommand = "magisk --install-module \"" + file.getAbsolutePath() + "\""; - installExecutable = new File(InstallerInitializer.peekMagiskPath() - .equals("/sbin") ? "/sbin/magisk" : "/system/bin/magisk"); + installExecutable = new File(MAGISK_PATH.equals("/sbin") ? + "/sbin/magisk" : "/system/bin/magisk"); } else if (moduleId != null) { installExecutable = this.extractInstallScript("module_installer_compat.sh"); if (installExecutable == null) { this.setInstallStateFinished(false, - "! Failed to extract module install script", null); + "! Failed to extract Magisk module install script", null); return; } - installCommand = "sh \"" + installExecutable.getAbsolutePath() + "\"" + - " /dev/null 1 \"" + file.getAbsolutePath() + "\""; + installCommand = ASH + " \"" + + installExecutable.getAbsolutePath() + "\"" + + " 3 1 \"" + file.getAbsolutePath() + "\""; } else { this.setInstallStateFinished(false, - "! Zip file is not a valid Magisk or a AnyKernel module!", null); + "! Zip file is not a valid Magisk or AnyKernel3 module!", null); return; } installerMonitor = new InstallerMonitor(installExecutable); if (moduleId != null) installerMonitor.setForCleanUp(moduleId); - if (anyKernel) { - final String adbTmp = "data/adb/tmp"; - final String anyKernelHome = // Mirror mounts do not contain nosuid - InstallerInitializer.peekMirrorPath() + - "/" + adbTmp + "/anykernel"; - installJob = Shell.cmd(arch32, "export MMM_EXT_SUPPORT=1", - "cd \"" + this.moduleCache.getAbsolutePath() + "\"", - "mkdir " + adbTmp, "export AKHOME=" + anyKernelHome, - installCommand).to(installerController, installerMonitor); - } else if (noExtensions) { - installJob = Shell.cmd(arch32, // No Extensions + if (noExtensions) { + installJob = Shell.cmd(arch32, "export BOOTMODE=true", // No Extensions "cd \"" + this.moduleCache.getAbsolutePath() + "\"", installCommand).to(installerController, installerMonitor); } else { @@ -381,7 +381,9 @@ public class InstallerActivity extends CompatActivity { Resources.getSystem().getConfiguration().locale.toLanguageTag()), "export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME, "export MMM_TEXT_WRAP=" + (this.textWrap ? "1" : "0"), - "cd \"" + this.moduleCache.getAbsolutePath() + "\"", + "export BOOTMODE=true", anyKernel3 ? "export AK3TMPFS=" + + InstallerInitializer.peekMagiskPath() + "/ak3tmpfs" : + "cd \"" + this.moduleCache.getAbsolutePath() + "\"", installCommand).to(installerController, installerMonitor); } } @@ -408,7 +410,8 @@ public class InstallerActivity extends CompatActivity { private final InstallerTerminal terminal; private final File moduleFile; private final boolean noExtension; - private boolean enabled, useExt; + private boolean enabled, useExt, + useRecovery, isRecoveryBar; private String supportLink = ""; private InstallerController(LinearProgressIndicator progressIndicator, @@ -431,7 +434,25 @@ public class InstallerActivity extends CompatActivity { return; } if (this.useExt && s.startsWith("#!")) { - this.processCommand(s); + this.processCommand(s.substring(2)); + } else if (this.useRecovery && s.startsWith("progress ")) { + String[] tokens = s.split(" "); + try { + float progress = Float.parseFloat(tokens[1]); + float max = Float.parseFloat(tokens[2]); + int progressInt; + if (max <= 0F) { + return; + } else if (progress >= max) { + progressInt = 256; + } else { + if (progress <= 0F) progress = 0F; + progressInt = (int) ((256D * progress) / max); + } + this.processCommand("showLoading 256"); + this.processCommand("setLoading " + progressInt); + this.isRecoveryBar = true; + } catch (Exception ignored) {} } else { this.terminal.addLine(s.replace( this.moduleFile.getAbsolutePath(), @@ -445,12 +466,15 @@ public class InstallerActivity extends CompatActivity { int i = rawCommand.indexOf(' '); if (i != -1) { arg = rawCommand.substring(i + 1).trim(); - command = rawCommand.substring(2, i); + command = rawCommand.substring(0, i); } else { arg = ""; - command = rawCommand.substring(2); + command = rawCommand; } switch (command) { + case "useRecovery": + this.useRecovery = true; + break; case "addLine": this.terminal.addLine(arg); break; @@ -467,6 +491,7 @@ public class InstallerActivity extends CompatActivity { this.terminal.scrollDown(); break; case "showLoading": + this.isRecoveryBar = false; if (!arg.isEmpty()) { try { short s = Short.parseShort(arg); @@ -481,7 +506,7 @@ public class InstallerActivity extends CompatActivity { } this.progressIndicator.setIndeterminate(true); } - } else if (!rawCommand.trim().equals("#!showLoading")) { + } else { this.progressIndicator.setProgressCompat(0, true); this.progressIndicator.setMax(100); if (this.progressIndicator.getVisibility() == View.VISIBLE) { @@ -492,6 +517,7 @@ public class InstallerActivity extends CompatActivity { this.progressIndicator.setVisibility(View.VISIBLE); break; case "setLoading": + this.isRecoveryBar = false; try { this.progressIndicator.setProgressCompat( Short.parseShort(arg), true); @@ -499,6 +525,7 @@ public class InstallerActivity extends CompatActivity { } break; case "hideLoading": + this.isRecoveryBar = false; this.progressIndicator.setVisibility(View.GONE); break; case "setSupportLink": @@ -510,8 +537,16 @@ public class InstallerActivity extends CompatActivity { } } + public void useRecoveryExt() { + this.useRecovery = true; + } + public void disable() { this.enabled = false; + if (this.isRecoveryBar) { + UiThreadHandler.runAndWait(() -> + this.processCommand("setLoading 256")); + } } public String getSupportLink() { diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java index 4fc7e76..17ea35e 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java @@ -36,6 +36,7 @@ public class InstallerInitializer extends Shell.Initializer { public static String peekMagiskPath() { return InstallerInitializer.MAGISK_PATH; } + public static String peekMirrorPath() { return InstallerInitializer.MAGISK_PATH == null ? null : InstallerInitializer.MAGISK_PATH + "/.magisk/mirror"; @@ -131,23 +132,6 @@ public class InstallerInitializer extends Shell.Initializer { public boolean onInit(@NonNull Context context, @NonNull Shell shell) { if (!shell.isRoot()) return true; - Shell.Job newJob = shell.newJob(); - String MAGISK_PATH = InstallerInitializer.MAGISK_PATH; - if (MAGISK_PATH == null) { - Log.w(TAG, "Unable to detect magisk path!"); - } else { - newJob.add("export ASH_STANDALONE=1"); - if (!MAGISK_PATH.equals("/sbin") && !MAGISK_SYSTEM.exists()) { - newJob.add("export PATH=" + MAGISK_PATH + ";$PATH;" + - MAGISK_PATH + "/.magisk/busybox"); - } else { - newJob.add("export PATH=$PATH;" + - MAGISK_PATH + "/.magisk/busybox"); - } - newJob.add("export MAGISKTMP=\"" + MAGISK_PATH + "/.magisk\""); - newJob.add("export BOOTMODE=true"); - newJob.add("$(which busybox 2> /dev/null) sh"); - } - return newJob.exec().isSuccess(); + return shell.newJob().add("export ASH_STANDALONE=1").exec().isSuccess(); } } diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java index 8bb50a7..c768002 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -86,8 +86,9 @@ public class RepoData { for (int i = 0; i < len; i++) { JSONObject module = array.getJSONObject(i); String moduleId = module.getString("id"); - // Deny remote modules ids shorter than 3 chars - if (moduleId.length() < 3) continue; + // Deny remote modules ids shorter than 3 chars or containing null char or space + if (moduleId.length() < 3 || moduleId.indexOf('\0') != -1 || + moduleId.indexOf(' ') != -1 || "ak3-helper".equals(moduleId)) continue; long moduleLastUpdate = module.getLong("last_update"); String moduleNotesUrl = module.getString("notes_url"); String modulePropsUrl = module.getString("prop_url"); diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java index 8d22ce1..3781e15 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java @@ -9,6 +9,7 @@ import org.json.JSONObject; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -19,7 +20,7 @@ public class RepoUpdater { public final RepoData repoData; public byte[] indexRaw; private List toUpdate; - private Set toApply; + private Collection toApply; public RepoUpdater(RepoData repoData) { this.repoData = repoData; @@ -36,7 +37,7 @@ public class RepoUpdater { if (!this.repoData.prepare()) { this.indexRaw = null; this.toUpdate = Collections.emptyList(); - this.toApply = Collections.emptySet(); + this.toApply = this.repoData.moduleHashMap.values(); return 0; } this.indexRaw = Http.doHttpGet(this.repoData.url, false); @@ -60,7 +61,7 @@ public class RepoUpdater { return this.toUpdate; } - public Set toApply() { + public Collection toApply() { return this.toApply; } diff --git a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java index fbf0989..15d1e4c 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java +++ b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java @@ -187,6 +187,9 @@ public class PropUtils { moduleInfo.description = value; readDescription = true; break; + case "updateJsonAk3": + // Only allow AnyKernel3 helper to use "updateJsonAk3" + if (!"ak3-helper".equals(moduleInfo.id)) break; case "updateJson": if (isInvalidURL(value)) break; moduleInfo.updateJson = value;