opt: use ksu.spawn for long script

This commit is contained in:
KOWX712
2025-05-05 18:57:24 +08:00
parent 59a74e8ee2
commit 8c4f7c0e5c
8 changed files with 248 additions and 206 deletions

View File

@@ -1,4 +1,7 @@
#!/bin/sh
# This file is the backend of JavaScript
MODPATH=${0%/*}
ORG_PATH="$PATH"
SKIPLIST="$MODPATH/tmp/skiplist"
@@ -17,13 +20,11 @@ aapt() { "$MODPATH/aapt" "$@"; }
# wget = low pref, no ssl.
# curl, has ssl on android, we use it if found
download() {
PATH=/data/adb/ap/bin:/data/adb/ksu/bin:/data/adb/magisk:/data/data/com.termux/files/usr/bin:$PATH
if command -v curl >/dev/null 2>&1; then
timeout 10 curl -Ls "$1"
else
timeout 10 busybox wget --no-check-certificate -qO- "$1"
fi
PATH="$ORG_PATH"
}
get_xposed() {
@@ -39,6 +40,19 @@ get_xposed() {
cat "$XPOSED"
}
get_applist() {
pm list packages -3 | awk -F: '{print $2}'
[ -s "/data/adb/tricky_store/system_app" ] && SYSTEM_APP=$(cat "/data/adb/tricky_store/system_app" | tr '\n' '|' | sed 's/|*$//') || SYSTEM_APP=""
[ -z "$SYSTEM_APP" ] || pm list packages -s | awk -F: '{print $2}' | grep -Ex "$SYSTEM_APP" || true
}
get_appname() {
base_apk=$(pm path $package_name | head -n1 | awk -F: '{print $2}')
app_name=$(aapt dump badging $base_apk 2>/dev/null | grep "application-label:" | sed "s/application-label://; s/'//g")
[ -z "$app_name" ] && app_name="$package_name"
echo "$app_name"
}
check_update() {
[ -f "$MODDIR/disable" ] && rm -f "$MODDIR/disable"
LOCAL_VERSION=$(grep '^versionCode=' "$MODPATH/update/module.prop" | awk -F= '{print $2}')
@@ -68,7 +82,6 @@ get_update() {
}
install_update() {
PATH=/data/adb/ap/bin:/data/adb/ksu/bin:/data/adb/magisk:$PATH
if command -v magisk >/dev/null 2>&1; then
magisk --install-module "$MODPATH/tmp/module.zip" || exit 1
elif command -v apd >/dev/null 2>&1; then
@@ -133,6 +146,15 @@ case "$1" in
get_xposed
exit
;;
--applist)
get_applist
exit
;;
--appname)
package_name="$2"
get_appname
exit
;;
--check-update)
REMOTE_VERSION="$2"
check_update

View File

@@ -1,41 +1,42 @@
import { exec, toast } from './assets/kernelsu.js';
import { basePath, hideFloatingBtn, appsWithExclamation, appsWithQuestion } from './main.js';
import { exec, spawn, toast } from './assets/kernelsu.js';
import { basePath, loadingIndicator, hideFloatingBtn, appsWithExclamation, appsWithQuestion, applyRippleEffect } from './main.js';
const appTemplate = document.getElementById('app-template').content;
const modeOverlay = document.querySelector('.mode-overlay');
export const appListContainer = document.getElementById('apps-list');
export const updateCard = document.getElementById('update-card');
let targetList = [];
// Fetch and render applist
export async function fetchAppList() {
try {
// fetch target list
let targetList = [];
await exec('cat /data/adb/tricky_store/target.txt')
.then(({ errno, stdout }) => {
if (errno === 0) {
targetList = processTargetList(stdout);
} else {
toast("Failed to read target.txt!");
}
});
// fetch target list
await exec('cat /data/adb/tricky_store/target.txt')
.then(({ errno, stdout }) => {
if (errno === 0) {
targetList = processTargetList(stdout);
} else {
toast("Failed to read target.txt!");
}
});
// fetch applist
const response = await fetch('applist.json');
const appList = await response.json();
const appNameMap = appList.reduce((map, app) => {
map[app.package_name] = app.app_name;
return map;
}, {});
// Fetch cached applist
const response = await fetch('applist.json');
const appList = await response.json();
const appNameMap = appList.reduce((map, app) => {
map[app.package_name] = app.app_name;
return map;
}, {});
// Get installed packages first
let appEntries = [], installedPackages = [];
const { stdout } = await exec(`
pm list packages -3 | awk -F: '{print $2}'
[ -s "/data/adb/tricky_store/system_app" ] && SYSTEM_APP=$(cat "/data/adb/tricky_store/system_app" | tr '\n' '|' | sed 's/|*$//') || SYSTEM_APP=""
[ -z "$SYSTEM_APP" ] || pm list packages -s | awk -F: '{print $2}' | grep -Ex "$SYSTEM_APP" || true
`);
installedPackages = stdout.split("\n").map(line => line.trim()).filter(Boolean);
// Get installed packages
let appEntries = [], installedPackages = [];
const output = spawn('sh', [`${basePath}/common/get_extra.sh`, '--applist']);
output.stdout.on('data', (data) => {
if (data.trim() === "") return;
installedPackages.push(data);
});
output.on('exit', async () => {
// Create appEntries array contain { appName, packageName }
appEntries = await Promise.all(installedPackages.map(async (packageName) => {
if (appNameMap[packageName]) {
return {
@@ -43,76 +44,86 @@ export async function fetchAppList() {
packageName
};
}
const { stdout: appName } = await exec(`
base_apk=$(pm path ${packageName} | head -n1 | awk -F: '{print $2}')
aapt dump badging $base_apk 2>/dev/null | grep "application-label:" | sed "s/application-label://; s/'//g"
`, { env: { PATH: `$PATH:${basePath}/common:/data/data/com.termux/files/usr/bin` } });
return {
appName: appName || packageName,
packageName
};
}));
// Sort
const sortedApps = appEntries.sort((a, b) => {
const aChecked = targetList.includes(a.packageName);
const bChecked = targetList.includes(b.packageName);
if (aChecked !== bChecked) {
return aChecked ? -1 : 1;
}
return (a.appName || "").localeCompare(b.appName || "");
});
// Render
appListContainer.innerHTML = "";
sortedApps.forEach(({ appName, packageName }) => {
const appElement = document.importNode(appTemplate, true);
const contentElement = appElement.querySelector(".content");
contentElement.setAttribute("data-package", packageName);
// Set unique names for radio button groups
const radioButtons = appElement.querySelectorAll('input[type="radio"]');
radioButtons.forEach((radio) => {
radio.name = `mode-radio-${packageName}`;
return new Promise((resolve) => {
const output = spawn('sh', [`${basePath}/common/get_extra.sh`, '--appname', packageName],
{ env: { PATH: `$PATH:${basePath}/common:/data/data/com.termux/files/usr/bin` } });
output.stdout.on('data', (data) => {
resolve({
appName: data,
packageName
});
});
});
}));
renderAppList(appEntries);
});
}
// Preselect the radio button based on the package name
const generateRadio = appElement.querySelector('#generate-mode');
const hackRadio = appElement.querySelector('#hack-mode');
const normalRadio = appElement.querySelector('#normal-mode');
/**
* Render processed app list to the UI
* @param {Array} data - Array of objects containing appName and packageName
* @returns {void}
*/
function renderAppList(data) {
// Sort
const sortedApps = data.sort((a, b) => {
const aChecked = targetList.includes(a.packageName);
const bChecked = targetList.includes(b.packageName);
if (aChecked !== bChecked) {
return aChecked ? -1 : 1;
}
return (a.appName || "").localeCompare(b.appName || "");
});
if (appsWithExclamation.includes(packageName)) {
generateRadio.checked = true;
} else if (appsWithQuestion.includes(packageName)) {
hackRadio.checked = true;
} else {
normalRadio.checked = true;
}
// Render
appListContainer.innerHTML = "";
sortedApps.forEach(({ appName, packageName }) => {
const appElement = document.importNode(appTemplate, true);
const contentElement = appElement.querySelector(".content");
contentElement.setAttribute("data-package", packageName);
const nameElement = appElement.querySelector(".name");
nameElement.innerHTML = `
<div class="app-info">
<div class="app-name"><strong>${appName}</strong></div>
<div class="package-name">${packageName}</div>
</div>
`;
const checkbox = appElement.querySelector(".checkbox");
checkbox.checked = targetList.includes(packageName);
appListContainer.appendChild(appElement);
// Set unique names for radio button groups
const radioButtons = appElement.querySelectorAll('input[type="radio"]');
radioButtons.forEach((radio) => {
radio.name = `mode-radio-${packageName}`;
});
} catch (error) {
toast("Failed to fetch app list!");
console.error("Failed to fetch or render app list with names:", error);
}
// Preselect the radio button based on the package name
const generateRadio = appElement.querySelector('#generate-mode');
const hackRadio = appElement.querySelector('#hack-mode');
const normalRadio = appElement.querySelector('#normal-mode');
if (appsWithExclamation.includes(packageName)) {
generateRadio.checked = true;
} else if (appsWithQuestion.includes(packageName)) {
hackRadio.checked = true;
} else {
normalRadio.checked = true;
}
const nameElement = appElement.querySelector(".name");
nameElement.innerHTML = `
<div class="app-info">
<div class="app-name"><strong>${appName}</strong></div>
<div class="package-name">${packageName}</div>
</div>
`;
const checkbox = appElement.querySelector(".checkbox");
checkbox.checked = targetList.includes(packageName);
appListContainer.appendChild(appElement);
});
loadingIndicator.style.display = "none";
hideFloatingBtn(false);
document.querySelector('.uninstall-container').classList.remove('hidden-uninstall');
toggleableCheckbox();
if (appListContainer.firstChild !== updateCard) {
appListContainer.insertBefore(updateCard, appListContainer.firstChild);
}
const checkboxes = appListContainer.querySelectorAll(".checkbox");
setupRadioButtonListeners();
setupModeMenu();
updateCheckboxColor();
applyRippleEffect();
}
// Function to save app with ! and ? then process target list

View File

@@ -41,6 +41,27 @@ export function exec(command, options = {}) {
});
}
/**
* Standard I/O stream for a child process.
* @class
*/
class Stdio {
constructor() {
this.listeners = {};
}
on(event, listener) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(listener);
}
emit(event, ...args) {
if (this.listeners[event]) {
this.listeners[event].forEach(listener => listener(...args));
}
}
}
/**
* Spawn shell process with ksu.spawn
* @param {string} command - The command to execute
@@ -71,20 +92,6 @@ export function spawn(command, args = [], options = {}) {
}
}
};
function Stdio() {
this.listeners = {};
}
Stdio.prototype.on = function(event, listener) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(listener);
};
Stdio.prototype.emit = function(event, ...args) {
if (this.listeners[event]) {
this.listeners[event].forEach(listener => listener(...args));
}
};
const callbackName = getUniqueCallbackName("spawn");
window[callbackName] = child;
child.on("exit", () => delete window[callbackName]);

View File

@@ -22,14 +22,12 @@ document.getElementById("boot-hash").addEventListener("click", async () => {
}, 10);
// read current boot hash
exec("cat /data/adb/boot_hash")
exec(`sed '/^#/d; /^$/d' /data/adb/boot_hash`)
.then(({ errno, stdout }) => {
if (errno !== 0) {
inputBox.value = "";
} else {
const validHash = stdout
.split("\n")
.filter(line => !line.startsWith("#") && line.trim())[0];
const validHash = stdout.trim();
inputBox.value = validHash || "";
}
});
@@ -48,7 +46,7 @@ const closeBootHashMenu = () => {
saveButton.addEventListener("click", async () => {
const inputValue = inputBox.value.trim();
exec(`
resetprop -n ro.boot.vbmeta.digest ${inputValue}
resetprop -n ro.boot.vbmeta.digest "${inputValue}"
[ -z "${inputValue}" ] && rm -f /data/adb/boot_hash || {
echo "${inputValue}" > /data/adb/boot_hash
chmod 644 /data/adb/boot_hash

View File

@@ -11,7 +11,7 @@ const title = document.querySelector('.header');
export const noConnection = document.querySelector('.no-connection');
// Loading, Save and Prompt Elements
const loadingIndicator = document.querySelector('.loading');
export const loadingIndicator = document.querySelector('.loading');
const prompt = document.getElementById('prompt');
const floatingCard = document.querySelector('.floating-card');
const floatingBtn = document.querySelector('.floating-btn');
@@ -50,16 +50,12 @@ export async function refreshAppList() {
appListContainer.innerHTML = '';
loadingIndicator.style.display = 'flex';
document.querySelector('.uninstall-container').classList.add('hidden-uninstall');
await new Promise(resolve => setTimeout(resolve, 500));
window.scrollTo(0, 0);
if (noConnection.style.display === "flex") {
updateCheck();
exec(`rm -f "${basePath}/common/tmp/exclude-list"`);
}
await fetchAppList();
applyRippleEffect();
loadingIndicator.style.display = 'none';
document.querySelector('.uninstall-container').classList.remove('hidden-uninstall');
fetchAppList();
isRefreshing = false;
}
@@ -201,7 +197,10 @@ function checkMMRL() {
// Funtion to adapt floating button hide in MMRL
export function hideFloatingBtn(hide = true) {
if (!hide) floatingCard.style.transform = 'translateY(0)';
if (!hide) {
floatingCard.style.transform = 'translateY(0)';
floatingBtn.style.display = 'block';
}
else floatingCard.style.transform = 'translateY(calc(var(--window-inset-bottom, 0px) + 120px))';
}
@@ -299,15 +298,10 @@ document.addEventListener('DOMContentLoaded', async () => {
setupMenuToggle();
setupLanguageMenu();
setupSystemAppMenu();
await fetchAppList();
applyRippleEffect();
fetchAppList();
checkTrickyStoreVersion();
checkMagisk();
updateCheck();
securityPatch();
loadingIndicator.style.display = "none";
floatingBtn.style.display = 'block';
hideFloatingBtn(false);
document.getElementById("refresh").addEventListener("click", refreshAppList);
document.querySelector('.uninstall-container').classList.remove('hidden-uninstall');
});

View File

@@ -1,4 +1,4 @@
import { exec } from './assets/kernelsu.js';
import { exec, spawn } from './assets/kernelsu.js';
import { basePath, showPrompt } from './main.js';
const overlay = document.getElementById('security-patch-overlay');
@@ -218,24 +218,29 @@ export function securityPatch() {
// Auto config button
autoButton.addEventListener('click', () => {
exec(`sh ${basePath}/common/get_extra.sh --security-patch`)
.then(({ errno, stdout }) => {
if (errno !== 0 || stdout.trim() === "not set") {
showPrompt('security_patch.auto_failed', false);
} else {
exec(`touch /data/adb/tricky_store/security_patch_auto_config`)
// Reset inputs
allPatchInput.value = '';
systemPatchInput.value = '';
bootPatchInput.value = '';
vendorPatchInput.value = '';
const output = spawn('sh', [`${basePath}/common/get_extra.sh`, '--security-patch']);
output.stdout.on('data', (data) => {
if (data.includes("not set")) {
showPrompt('security_patch.auto_failed', false);
}
});
output.on('exit', (code) => {
if (code === 0) {
exec(`touch /data/adb/tricky_store/security_patch_auto_config`)
// Reset inputs
allPatchInput.value = '';
systemPatchInput.value = '';
bootPatchInput.value = '';
vendorPatchInput.value = '';
checkAdvanced(false);
showPrompt('security_patch.auto_success');
}
hideSecurityPatchDialog();
loadCurrentConfig();
});
checkAdvanced(false);
showPrompt('security_patch.auto_success');
} else {
showPrompt('security_patch.auto_failed', false);
}
hideSecurityPatchDialog();
loadCurrentConfig();
});
});
// Save button
@@ -308,21 +313,18 @@ export function securityPatch() {
// Get button
getButton.addEventListener('click', async () => {
showPrompt('security_patch.fetching');
setTimeout(() => {
exec(`sh ${basePath}/common/get_extra.sh --get-security-patch`)
.then(({ errno, stdout }) => {
if (errno !== 0) {
showPrompt('security_patch.get_failed', false);
} else {
showPrompt('security_patch.fetched', true, 1000);
checkAdvanced(true);
const output = spawn('sh', [`${basePath}/common/get_extra.sh`, '--get-security-patch']);
output.stdout.on('data', (data) => {
showPrompt('security_patch.fetched', true, 1000);
checkAdvanced(true);
allPatchInput.value = stdout.replace(/-/g, '');
systemPatchInput.value = 'prop';
bootPatchInput.value = stdout;
vendorPatchInput.value = stdout;
}
})
}, 200);
allPatchInput.value = data.replace(/-/g, '');
systemPatchInput.value = 'prop';
bootPatchInput.value = data;
vendorPatchInput.value = data;
});
output.on('exit', (code) => {
if (code !== 0) showPrompt('security_patch.get_failed', false);
});
});
}

View File

@@ -1,4 +1,4 @@
import { exec } from './assets/kernelsu.js';
import { exec, spawn } from './assets/kernelsu.js';
import { basePath, showPrompt, noConnection, linkRedirect } from './main.js';
import { updateCard } from './applist.js';
@@ -52,12 +52,14 @@ export async function updateCheck() {
zipURL = data.zipUrl;
changelogURL = data.changelog;
const { stdout } = await exec(`sh ${basePath}/common/get_extra.sh --check-update ${remoteVersionCode}`);
if (stdout.includes("update")) {
showPrompt("prompt.new_update", true, 1500);
updateCard.style.display = "flex";
setupUpdateMenu();
}
const output = spawn('sh', [`${basePath}/common/get_extra.sh`, '--check-update', `${remoteVersionCode}`]);
output.stdout.on('data', (data) => {
if (data.includes("update")) {
showPrompt("prompt.new_update", true, 1500);
updateCard.style.display = "flex";
setupUpdateMenu();
}
});
} catch (error) {
console.error("Error fetching JSON or executing command:", error);
showPrompt("prompt.no_internet", false);
@@ -111,43 +113,41 @@ function setupUpdateMenu() {
// Update card
updateCard.addEventListener('click', async () => {
try {
const { stdout } = await exec(`
[ -f ${basePath}/common/tmp/module.zip ] || echo "noModule"
[ -f ${basePath}/common/tmp/changelog.md ] || echo "noChangelog"
[ ! -f /data/adb/modules/TA_utl/update ] || echo "updated"
`);
if (stdout.trim().includes("updated")) {
installButton.style.display = "none";
rebootButton.style.display = "flex";
openUpdateMenu();
} else if (stdout.trim().includes("noChangelog")) {
showPrompt("prompt.downloading");
await downloadFile(changelogURL, "changelog.md");
renderChangelog();
openUpdateMenu();
setTimeout(() => {
updateCard.click();
}, 200);
} else if (stdout.trim().includes("noModule")) {
if (downloading) return;
downloading = true;
const { errno } = await exec(`sh ${basePath}/common/get_extra.sh --get-update ${zipURL}`);
if (errno === 0) {
const { stdout } = await exec(`
[ -f ${basePath}/common/tmp/module.zip ] || echo "noModule"
[ -f ${basePath}/common/tmp/changelog.md ] || echo "noChangelog"
[ ! -f /data/adb/modules/TA_utl/update ] || echo "updated"
`);
if (stdout.trim().includes("updated")) {
installButton.style.display = "none";
rebootButton.style.display = "flex";
openUpdateMenu();
} else if (stdout.trim().includes("noChangelog")) {
showPrompt("prompt.downloading");
await downloadFile(changelogURL, "changelog.md");
renderChangelog();
openUpdateMenu();
setTimeout(() => {
updateCard.click();
}, 200);
} else if (stdout.trim().includes("noModule")) {
if (downloading) return;
downloading = true;
const download = spawn('sh', [`${basePath}/common/get_extra.sh`, '--get-update', `${zipURL}`],
{ env: { PATH: "$PATH:/data/adb/ap/bin:/data/adb/ksu/bin:/data/adb/magisk:/data/data/com.termux/files/usr/bin" } });
download.on('exit', (code) => {
downloading = false;
if (code === 0) {
showPrompt("prompt.downloaded");
installButton.style.display = "flex";
} else {
showPrompt("prompt.download_fail", false);
}
downloading = false;
} else {
installButton.style.display = "flex";
renderChangelog();
openUpdateMenu();
}
} catch (error) {
showPrompt("prompt.download_fail", false);
console.error('Error download module update:', error);
});
} else {
installButton.style.display = "flex";
renderChangelog();
openUpdateMenu();
}
});
@@ -160,16 +160,20 @@ function setupUpdateMenu() {
// Install button
installButton.addEventListener('click', async () => {
showPrompt("prompt.installing");
await new Promise(resolve => setTimeout(resolve, 300));
const { errno, stderr } = await exec(`sh ${basePath}/common/get_extra.sh --install-update`);
if (errno === 0) {
showPrompt("prompt.installed");
installButton.style.display = "none";
rebootButton.style.display = "flex";
} else {
showPrompt("prompt.install_fail", false);
console.error('Fail to execute installation script:', stderr);
}
const output = spawn('sh', [`${basePath}/common/get_extra.sh`, '--install-update'],
{ env: { PATH: "$PATH:/data/adb/ap/bin:/data/adb/ksu/bin:/data/adb/magisk" } });
output.stderr.on('data', (data) => {
console.error('Error during installation:', data);
})
output.on('exit', (code) => {
if (code === 0) {
showPrompt("prompt.installed");
installButton.style.display = "none";
rebootButton.style.display = "flex";
} else {
showPrompt("prompt.install_fail", false);
}
});
});
// Reboot button

View File

@@ -58,12 +58,16 @@
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
.update-content h1,
.update-content h3 {
font-size: 24px;
margin: 10px 0;
margin-top: 0;
}
.update-content h3 {
font-size: 24px;
}
.changelog {
max-height: 65vh;
overflow-y: auto;