From a75762e9b77e90727c80db4508ebf33c1889503a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BE=84=E9=97=AA?= Date: Thu, 19 Mar 2026 21:06:20 +0800 Subject: [PATCH] Add diagnostics, suicide delay, and display detection --- .../java/eu/chainfire/liveboot/Installer.java | 84 +++- .../java/eu/chainfire/liveboot/Settings.java | 1 + .../chainfire/liveboot/SettingsFragment.java | 51 ++- .../eu/chainfire/liveboot/shell/Runner.java | 406 +++++++++++++++--- liveBootAni2/src/main/res/values/strings.xml | 2 + 5 files changed, 482 insertions(+), 62 deletions(-) diff --git a/liveBootAni2/src/main/java/eu/chainfire/liveboot/Installer.java b/liveBootAni2/src/main/java/eu/chainfire/liveboot/Installer.java index 78b611c..c9fbf51 100644 --- a/liveBootAni2/src/main/java/eu/chainfire/liveboot/Installer.java +++ b/liveBootAni2/src/main/java/eu/chainfire/liveboot/Installer.java @@ -21,14 +21,18 @@ import android.app.ProgressDialog; import android.content.Context; import android.graphics.Point; +import android.hardware.display.DisplayManager; import android.os.AsyncTask; import android.os.Build; import android.os.StatFs; +import android.view.Display; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import eu.chainfire.librootjava.AppProcess; import eu.chainfire.librootjava.Logger; @@ -42,7 +46,7 @@ public class Installer { public enum Mode { SU_D, INIT_D, SU_SU_D, SBIN_SU_D, MAGISK_CORE, MAGISK_ADB, KERNELSU } - private static final int LAST_SCRIPT_UPDATE = 188; + private static final int LAST_SCRIPT_UPDATE = 195; private static final String[] SYSTEM_SCRIPTS_SU_D = new String[] { "/system/su.d/0000liveboot" }; private static final String[] SYSTEM_SCRIPTS_INIT_D = new String[] { "/system/etc/init.d/0000liveboot" }; private static final String[] SYSTEM_SCRIPTS_SU_SU_D = new String[] { "/su/su.d/0000liveboot" }; @@ -136,22 +140,64 @@ public static boolean installNeeded(Context context, Mode mode) { return installNeededVersion(settings) || installNeededData(context) || installNeededScript(context, mode); } - public static synchronized Point getScreenDimensions() { + private static long getArea(int width, int height) { + return (long) width * (long) height; + } + + private static boolean isBetterDimensions(int width, int height, Point currentBest) { + if (width <= 0 || height <= 0) return false; + long candidateArea = getArea(width, height); + long currentArea = getArea(currentBest.x, currentBest.y); + return (candidateArea > currentArea) || + ((candidateArea == currentArea) && ((width > currentBest.x) || ((width == currentBest.x) && (height > currentBest.y)))); + } + + private static Point getScreenDimensionsFromDisplayManager(Context context) { Point ret = new Point(0, 0); + try { + DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + if (displayManager != null) { + Display defaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + if (defaultDisplay != null) { + Point size = new Point(); + defaultDisplay.getRealSize(size); + if (size.x > 0 && size.y > 0) { + ret.set(size.x, size.y); + return ret; + } + } + + Display[] displays = displayManager.getDisplays(); + if (displays != null) { + for (Display display : displays) { + if (display == null) continue; + Point size = new Point(); + display.getRealSize(size); + if (isBetterDimensions(size.x, size.y, ret)) { + ret.set(size.x, size.y); + } + } + } + } + } catch(Exception e) { + Logger.ex(e); + } + return ret; + } + + private static Point getScreenDimensionsFromDumpsys() { + Point ret = new Point(0, 0); + Pattern realPattern = Pattern.compile("\\breal\\s+(\\d+)\\s*x\\s*(\\d+)\\b", Pattern.CASE_INSENSITIVE); try { List output = Shell.SU.run("dumpsys display | grep -i real | grep -vi overridedisplay"); if (output != null) { for (String line : output) { - String[] parts = line.split(","); - for (int i = 0; i < parts.length; i++) { - if (parts[i].contains("real")) { - String[] sub = parts[i].split(" "); - for (int j = 0; j < sub.length; j++) { - if (sub[j].equals("real")) { - ret.x = Integer.valueOf(sub[j + 1], 10); - ret.y = Integer.valueOf(sub[j + 3], 10); - } - } + Matcher matcher = realPattern.matcher(line); + while (matcher.find()) { + int width = Integer.parseInt(matcher.group(1), 10); + int height = Integer.parseInt(matcher.group(2), 10); + if (isBetterDimensions(width, height, ret)) { + ret.set(width, height); } } } @@ -161,6 +207,17 @@ public static synchronized Point getScreenDimensions() { } return ret; } + + public static synchronized Point getScreenDimensions(Context context) { + Point ret = new Point(0, 0); + if (context != null) { + ret = getScreenDimensionsFromDisplayManager(directBootContext(context)); + } + if (ret.x <= 0 || ret.y <= 0) { + ret = getScreenDimensionsFromDumpsys(); + } + return ret; + } public static synchronized List getLaunchScript(Context context, boolean boot) { Settings settings = Settings.getInstance(context); @@ -187,9 +244,10 @@ public static synchronized List getLaunchScript(Context context, boolean if (!settings.LOGCAT_COLORS.get()) params.add("logcatnocolors"); params.add("dmesg=" + ((settings.DMESG.get() && (boot || !haveLogcat)) ? Settings.DMESG_ALL : Settings.DMESG_NONE)); params.add("lines=" + settings.LINES.get()); + params.add("suicidedelay=" + settings.SUICIDE_DELAY_MS.get()); if (settings.WORD_WRAP.get()) params.add("wordwrap"); if (settings.SAVE_LOGS.get() && boot) params.add("save"); - Point dms = getScreenDimensions(); + Point dms = getScreenDimensions(context); params.add("fallbackwidth=" + dms.x); params.add("fallbackheight=" + dms.y); String relocate = AppProcess.shouldAppProcessBeRelocated() ? "/dev" : null; diff --git a/liveBootAni2/src/main/java/eu/chainfire/liveboot/Settings.java b/liveBootAni2/src/main/java/eu/chainfire/liveboot/Settings.java index 4ea233c..81abeb6 100644 --- a/liveBootAni2/src/main/java/eu/chainfire/liveboot/Settings.java +++ b/liveBootAni2/src/main/java/eu/chainfire/liveboot/Settings.java @@ -209,6 +209,7 @@ public SharedPreferences getPrefs() { public BooleanSetting DMESG = new BooleanSetting(this, "dmesg", true); public StringSetting LINES = new StringSetting(this, "lines", "80"); + public StringSetting SUICIDE_DELAY_MS = new StringSetting(this, "suicide_delay_ms", "0"); public BooleanSetting WORD_WRAP = new BooleanSetting(this, "word_wrap", true); public BooleanSetting SAVE_LOGS = new BooleanSetting(this, "save_logs", false); diff --git a/liveBootAni2/src/main/java/eu/chainfire/liveboot/SettingsFragment.java b/liveBootAni2/src/main/java/eu/chainfire/liveboot/SettingsFragment.java index fa4a380..9d689ea 100644 --- a/liveBootAni2/src/main/java/eu/chainfire/liveboot/SettingsFragment.java +++ b/liveBootAni2/src/main/java/eu/chainfire/liveboot/SettingsFragment.java @@ -31,6 +31,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.MultiSelectListPreference; import android.preference.Preference; @@ -39,6 +40,7 @@ import android.preference.PreferenceCategory; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; +import android.text.InputType; import eu.chainfire.librootjava.Logger; import eu.chainfire.libsuperuser.Shell; @@ -51,6 +53,8 @@ import java.util.Set; public class SettingsFragment extends PreferenceFragment implements OnSharedPreferenceChangeListener { + private static final int MAX_SUICIDE_DELAY_MS = 60000; + private String APP_TITLE = ""; private SharedPreferences prefs = null; private Settings settings = null; @@ -59,6 +63,7 @@ public class SettingsFragment extends PreferenceFragment implements OnSharedPref private MultiSelectListPreference prefLogcatBuffers = null; private ListPreference prefLogcatFormat = null; private ListPreference prefLines = null; + private EditTextPreference prefSuicideDelay = null; private InAppPurchases iap = null; private volatile boolean pro = false; @@ -397,6 +402,21 @@ private void disableIfNoRoot(Activity activity, Preference preference, boolean h } } + private String normalizeSuicideDelayValue(Object value) { + String normalized = value == null ? settings.SUICIDE_DELAY_MS.defaultValue : String.valueOf(value).trim(); + if (normalized.length() == 0) { + normalized = settings.SUICIDE_DELAY_MS.defaultValue; + } + try { + long parsed = Long.parseLong(normalized, 10); + if (parsed < 0) parsed = 0; + if (parsed > MAX_SUICIDE_DELAY_MS) parsed = MAX_SUICIDE_DELAY_MS; + return String.valueOf(parsed); + } catch (Exception e) { + return settings.SUICIDE_DELAY_MS.defaultValue; + } + } + private PreferenceScreen createPreferenceHierarchy(boolean haveRoot) { final Activity activity = getActivity(); if (activity == null) return null; @@ -574,6 +594,21 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { "160" }; prefLines = Pref.List(activity, catOptions, R.string.settings_lines_title, 0, R.string.settings_lines_title, settings.LINES.name, settings.LINES.defaultValue, lines, lines, true); + + prefSuicideDelay = Pref.Edit(activity, catOptions, R.string.settings_suicide_delay_title, 0, R.string.settings_suicide_delay_title, settings.SUICIDE_DELAY_MS.name, settings.SUICIDE_DELAY_MS.defaultValue, InputType.TYPE_CLASS_NUMBER); + prefSuicideDelay.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + EditTextPreference editPreference = (EditTextPreference) preference; + String normalized = normalizeSuicideDelayValue(newValue); + editPreference.setText(normalized); + if (!normalized.equals(String.valueOf(newValue).trim())) { + settings.SUICIDE_DELAY_MS.set(normalized); + return false; + } + return true; + } + }); Pref.Check(activity, catOptions, R.string.settings_wordwrap_title, R.string.settings_wordwrap_description, settings.WORD_WRAP.name, settings.WORD_WRAP.defaultValue); @@ -774,7 +809,21 @@ private void updatePrefs(String key) { )); } } - + + if ((key == null) || key.equals(settings.SUICIDE_DELAY_MS.name)) { + String normalized = normalizeSuicideDelayValue(settings.SUICIDE_DELAY_MS.get()); + if (!normalized.equals(settings.SUICIDE_DELAY_MS.get())) { + settings.SUICIDE_DELAY_MS.set(normalized); + } + if (prefSuicideDelay != null) { + prefSuicideDelay.setText(normalized); + prefSuicideDelay.setSummary(String.format(Locale.ENGLISH, "%s\n[ %s ms ]", + getString(R.string.settings_suicide_delay_description), + normalized + )); + } + } + if (key != null) { if (activity != null) { (new Thread(new Runnable() { diff --git a/liveBootAni2/src/main/java/eu/chainfire/liveboot/shell/Runner.java b/liveBootAni2/src/main/java/eu/chainfire/liveboot/shell/Runner.java index b37cabb..2d838be 100644 --- a/liveBootAni2/src/main/java/eu/chainfire/liveboot/shell/Runner.java +++ b/liveBootAni2/src/main/java/eu/chainfire/liveboot/shell/Runner.java @@ -25,7 +25,13 @@ import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import eu.chainfire.librootjava.Logger; @@ -48,6 +54,8 @@ public class OnLineListener, SurfaceHost.IGLRenderCallback { + private static volatile Runner sCurrentRunner = null; + public static void main(String[] args) { Logger.setLogTag("LiveBootSurface"); Logger.setDebugLogging(BuildConfig.DEBUG); @@ -62,6 +70,9 @@ public static void main(String[] args) { @Override public void uncaughtException(Thread thread, Throwable throwable) { Logger.dp("EXCEPTION", "%s", throwable.getClass().getName()); + if (sCurrentRunner != null) { + sCurrentRunner.diagException("EXCEPTION", throwable); + } if (oldHandler != null) { oldHandler.uncaughtException(thread, throwable); } else { @@ -70,19 +81,29 @@ public void uncaughtException(Thread thread, Throwable throwable) { } }); - new Runner().run(args); + Runner runner = new Runner(); + sCurrentRunner = runner; + runner.run(args); } public static final String LIVEBOOT_ABORT_FILE = "/dev/.liveboot_exit"; private static final int TEST_TIME = 5000; private static final int LEAD_TIME = 200; - private static final int FOLLOW_TIME_SCRIPT = 60000; + private static final int FOLLOW_TIME_SCRIPT = 60000; + private static final int FALLBACK_BOOT_SIGNAL_WAIT = 10000; + private static final int FAILSAFE_MAX_RUNTIME = 240000; + private static final int BOOT_STATE_LOG_INTERVAL = 2000; + private static final int MAX_SUICIDE_DELAY_MS = 60000; + private static final String[] BOOT_ANIMATION_PROCESS_HINTS = new String[] { "bootanim", "bootanimation" }; + private static final String[] BOOT_ANIMATION_SERVICE_HINTS = new String[] { "bootanim", "bootanimation" }; + private static final String[] DIAG_LOG_NAMES = new String[] { "/cache/liveboot.diag.log", "/data/local/tmp/liveboot.diag.log" }; private boolean mTest = false; private int mWidth = 0; private int mHeight = 0; private int mLines = 80; + private int mSuicideDelayMs = 0; private boolean mWordWrap = false; private boolean mTransparent = false; private boolean mDark = false; @@ -91,6 +112,12 @@ public void uncaughtException(Thread thread, Throwable throwable) { private boolean mLogSave = false; private OutputStream mLogStream = null; private ReentrantLock mLogLock = new ReentrantLock(true); + private String mDiagLogName = null; + private OutputStream mDiagStream = null; + private ReentrantLock mDiagLock = new ReentrantLock(true); + private PrintStream mDiagPrintStream = null; + private PrintStream mOldErr = null; + private PrintStream mOldOut = null; private static final String SCRIPT_NAME_SYSTEM = "/system/su.d/0000liveboot.script"; private static final String SCRIPT_NAME_SU = "/su/su.d/0000liveboot.script"; private static final String SCRIPT_NAME_SBIN = "/sbin/supersu/su.d/0000liveboot.script"; @@ -110,39 +137,263 @@ public void uncaughtException(Thread thread, Throwable throwable) { private long mFirstLine = 0; private int mLinesPassed = 0; + private int mDroppedLines = 0; + private boolean mLoggedFirstLogcatLine = false; + private boolean mLoggedFirstDmesgLine = false; + private boolean mLoggedFirstScriptLine = false; + private long mLastBootStateLog = -BOOT_STATE_LOG_INTERVAL; + private String mLastBootStateSummary = null; private volatile long mComplete = 0; - - private boolean isBootAnimationRunning() { - List ps = Shell.SH.run(new String[] { - "/system/bin/" + Toolbox.command("ps") + " | /system/bin/" + Toolbox.command("grep") + " bootanim"+ " | /system/bin/" + Toolbox.command("grep") + " -v grep", - "/system/bin/" + Toolbox.command("ps") + " -A | /system/bin/" + Toolbox.command("grep") + " bootanim" + " | /system/bin/" + Toolbox.command("grep") + " -v grep" - }); - if (ps != null) { - for (String line : ps) { - if (line.contains("bootanim")) { - return true; + + private void openDiagLog() { + for (String fileName : DIAG_LOG_NAMES) { + try { + mDiagStream = new FileOutputStream(fileName, false); + mDiagLogName = fileName; + break; + } catch (Exception e) { + } + } + } + + private void redirectStandardStreams() { + if (mDiagLogName == null) return; + try { + mDiagPrintStream = new PrintStream(new FileOutputStream(mDiagLogName, true), true); + mOldErr = System.err; + mOldOut = System.out; + System.setErr(mDiagPrintStream); + System.setOut(mDiagPrintStream); + } catch (Exception e) { + } + } + + private void restoreStandardStreams() { + try { + if (mOldErr != null) { + System.setErr(mOldErr); + } + if (mOldOut != null) { + System.setOut(mOldOut); + } + } catch (Exception e) { + } + try { + if (mDiagPrintStream != null) { + mDiagPrintStream.close(); + } + } catch (Exception e) { + } + mDiagPrintStream = null; + mOldErr = null; + mOldOut = null; + } + + private void closeDiagLog() { + restoreStandardStreams(); + mDiagLock.lock(); + try { + if (mDiagStream != null) { + try { + mDiagStream.close(); + } catch (Exception e) { } + mDiagStream = null; } + } finally { + mDiagLock.unlock(); } - return SystemProperties.get("init.svc.bootanim", "stopped").equals("running"); } - private void killBootAnimation() { - List ps = Shell.SH.run(new String[] { - "/system/bin/" + Toolbox.command("ps") + " | /system/bin/" + Toolbox.command("grep") + " bootanim"+ " | /system/bin/" + Toolbox.command("grep") + " -v grep", - "/system/bin/" + Toolbox.command("ps") + " -A | /system/bin/" + Toolbox.command("grep") + " bootanim" + " | /system/bin/" + Toolbox.command("grep") + " -v grep" + private void diag(String tag, String format, Object... params) { + OutputStream diagStream = mDiagStream; + if (diagStream == null) return; + + String message; + try { + message = (params == null || params.length == 0) ? format : String.format(Locale.ENGLISH, format, params); + } catch (Exception e) { + message = format; + } + String line = String.format(Locale.ENGLISH, "[%8d][%s] %s\n", SystemClock.elapsedRealtime(), tag, message); + + mDiagLock.lock(); + try { + if (mDiagStream != null) { + mDiagStream.write(line.getBytes()); + mDiagStream.flush(); + } + } catch (Exception e) { + } finally { + mDiagLock.unlock(); + } + } + + private void diagException(String tag, Throwable throwable) { + if (throwable == null) return; + diag(tag, "%s: %s", throwable.getClass().getName(), String.valueOf(throwable.getMessage())); + } + + private String shorten(String text) { + if (text == null) return ""; + text = text.replace('\n', ' ').replace('\r', ' '); + if (text.length() > 160) { + return text.substring(0, 160); + } + return text; + } + + private String getSenderName(Object sender) { + if (sender == mLogcat) return "logcat"; + if (sender == mDmesg) return "dmesg"; + if (sender == mScript) return "script"; + if (sender == null) return "null"; + return sender.getClass().getSimpleName(); + } + + private void maybeLogFirstLine(Object sender, String text) { + if ((sender == mLogcat) && !mLoggedFirstLogcatLine) { + mLoggedFirstLogcatLine = true; + diag("LINES", "first logcat line: %s", shorten(text)); + } else if ((sender == mDmesg) && !mLoggedFirstDmesgLine) { + mLoggedFirstDmesgLine = true; + diag("LINES", "first dmesg line: %s", shorten(text)); + } else if ((sender == mScript) && !mLoggedFirstScriptLine) { + mLoggedFirstScriptLine = true; + diag("LINES", "first script line: %s", shorten(text)); + } + } + + private String getInterestingProperties() { + return String.format( + Locale.ENGLISH, + "init.svc.bootanim=%s init.svc.bootanimation=%s service.bootanim.exit=%s service.bootanim.completed=%s sys.boot_completed=%s dev.bootcomplete=%s persist.sys.multi_display_type=%s", + SystemProperties.get("init.svc.bootanim", "stopped"), + SystemProperties.get("init.svc.bootanimation", "stopped"), + SystemProperties.get("service.bootanim.exit", "0"), + SystemProperties.get("service.bootanim.completed", "0"), + SystemProperties.get("sys.boot_completed", "0"), + SystemProperties.get("dev.bootcomplete", "0"), + SystemProperties.get("persist.sys.multi_display_type", "") + ); + } + + private void maybeLogBootState(long elapsed, boolean bootAnimationSeen, boolean bootAnimationGone, boolean bootAnimationRunning, List pids, long complete, String completeSignal) { + String summary = String.format( + Locale.ENGLISH, + "elapsed=%d running=%s seen=%s gone=%s complete=%s signal=%s pids=%s props={%s}", + elapsed, + String.valueOf(bootAnimationRunning), + String.valueOf(bootAnimationSeen), + String.valueOf(bootAnimationGone), + String.valueOf(complete > 0), + (completeSignal == null ? "-" : completeSignal), + pids.toString(), + getInterestingProperties() + ); + if (!summary.equals(mLastBootStateSummary) || (elapsed - mLastBootStateLog >= BOOT_STATE_LOG_INTERVAL)) { + diag("BOOT", summary); + mLastBootStateSummary = summary; + mLastBootStateLog = elapsed; + } + } + + private boolean isPropertyOne(String key) { + return SystemProperties.get(key, "0").equals("1"); + } + + private String getBootCompletionSignal(boolean bootAnimationSeen, boolean bootAnimationRunning, long elapsed) { + if (isPropertyOne("service.bootanim.exit")) { + return "service.bootanim.exit"; + } + if (isPropertyOne("service.bootanim.completed")) { + return "service.bootanim.completed"; + } + + // Some ROMs no longer expose service.bootanim.* during boot. + if ( + (elapsed >= FALLBACK_BOOT_SIGNAL_WAIT) && + (bootAnimationSeen || !bootAnimationRunning) + ) { + if (isPropertyOne("sys.boot_completed")) { + return "sys.boot_completed"; + } + if (isPropertyOne("dev.bootcomplete")) { + return "dev.bootcomplete"; + } + } + return null; + } + + private List getProcessList() { + return Shell.SH.run(new String[] { + "/system/bin/" + Toolbox.command("ps"), + "/system/bin/" + Toolbox.command("ps") + " -A" }); + } + + private Integer getPidFromPsLine(String line) { + if (line == null) return null; + String[] parts = line.trim().split(" +"); + for (String part : parts) { + try { + int pid = Integer.valueOf(part, 10); + if (pid > 1) { + return pid; + } + } catch (Exception e) { + } + } + return null; + } + + private boolean isBootAnimationProcessLine(String line) { + if (line == null) return false; + String lower = line.toLowerCase(Locale.ENGLISH); + for (String hint : BOOT_ANIMATION_PROCESS_HINTS) { + if (lower.contains(hint)) { + return true; + } + } + return false; + } + + private List getBootAnimationPids() { + List ret = new ArrayList(); + Set seen = new HashSet(); + List ps = getProcessList(); if (ps != null) { for (String line : ps) { - String[] parts = line.split(" +"); - if (parts.length >= 2) { - Shell.run("sh", new String[] { "/system/bin/" + Toolbox.command("kill") + " -9 " + parts[1] }, null, false); + if (isBootAnimationProcessLine(line)) { + Integer pid = getPidFromPsLine(line); + if ((pid != null) && !seen.contains(pid)) { + seen.add(pid); + ret.add(pid); + } } } - } + } + return ret; } - + + private boolean isBootAnimationServiceRunning() { + for (String service : BOOT_ANIMATION_SERVICE_HINTS) { + if (SystemProperties.get("init.svc." + service, "stopped").equals("running")) { + return true; + } + } + return false; + } + + private boolean isBootAnimationRunning() { + return isBootAnimationRunning(getBootAnimationPids()); + } + + private boolean isBootAnimationRunning(List pids) { + return !pids.isEmpty() || isBootAnimationServiceRunning(); + } + private void infanticide() { // children String pid = String.valueOf(android.os.Process.myPid()); List ps = Shell.SH.run(new String[] { @@ -169,12 +420,14 @@ private void suicide() { // self protected void onSize(int width, int height) { mWidth = width; mHeight = height; + diag("SURFACE", "size=%dx%d", width, height); } @Override protected void onResize(int width, int height) { mWidth = width; mHeight = height; + diag("SURFACE", "resize=%dx%d", width, height); mTextManager.resize(-1, -1, width, height, -1, mHeight / mLines); mHelper.resize(width, height); } @@ -184,6 +437,11 @@ protected void onInit(String[] args) { RootDaemon.daemonize(BuildConfig.APPLICATION_ID, 0, false, null); Toolbox.init(); + openDiagLog(); + redirectStandardStreams(); + diag("INIT", "pid=%d args=%s", android.os.Process.myPid(), Arrays.toString(args)); + diag("INIT", "properties={%s}", getInterestingProperties()); + diag("INIT", "diag log=%s stderr_redirect=%s", String.valueOf(mDiagLogName), String.valueOf(mDiagPrintStream != null)); // parse options String logcatLevelOpts = null; @@ -224,6 +482,9 @@ protected void onInit(String[] args) { } else if (key.equals("lines")) { mLines = Integer.valueOf(value, 10); Logger.dp("OPTS", "mLines==%s", mLines); + } else if (key.equals("suicidedelay")) { + mSuicideDelayMs = Math.max(0, Math.min(MAX_SUICIDE_DELAY_MS, Integer.valueOf(value, 10))); + Logger.dp("OPTS", "mSuicideDelayMs==%d", mSuicideDelayMs); } else if (key.equals("logcatlevels")) { logcatLevelOpts = value; Logger.dp("OPTS", "logcatLevelOpts==%s", logcatLevelOpts); @@ -240,6 +501,7 @@ protected void onInit(String[] args) { } } catch (Exception e) { Logger.ex(e); + diagException("INIT", e); } } @@ -254,11 +516,31 @@ protected void onInit(String[] args) { mHandlerThread = new HandlerThread("LiveBoot HandlerThread"); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); + diag( + "INIT", + "runScript=%s test=%s transparent=%s dark=%s wordwrap=%s save=%s lines=%d suicideDelayMs=%d fallback=%dx%d logcatLevels=%s logcatBuffers=%s logcatFormat=%s dmesg=%s", + (mRunScript == null ? "" : mRunScript), + String.valueOf(mTest), + String.valueOf(mTransparent), + String.valueOf(mDark), + String.valueOf(mWordWrap), + String.valueOf(mLogSave), + mLines, + mSuicideDelayMs, + fallbackWidth, + fallbackHeight, + String.valueOf(logcatLevelOpts), + String.valueOf(logcatBufferOpts), + String.valueOf(logcatFormatOpt), + String.valueOf(dmesgOpts) + ); if (mLogSave) { try { mLogStream = new FileOutputStream(LOG_NAME, false); + diag("INIT", "log stream opened at %s", LOG_NAME); } catch (Exception e) { + diagException("INIT", e); } } @@ -266,19 +548,26 @@ protected void onInit(String[] args) { if (mRunScript == null) { mLogcat = new Logcat(this, mLines * 4, logcatLevelOpts, logcatBufferOpts, logcatFormatOpt, mHandler); mDmesg = new Dmesg(this, mLines * 4, dmesgOpts, mHandler); + diag("INIT", "started direct log sources"); + } else { + diag("INIT", "waiting for script mode source"); } } @Override protected void onDone() { + diag("DONE", "onDone start"); mHandlerThread.quit(); if (mLogcat != null) mLogcat.destroy(); if (mDmesg != null) mDmesg.destroy(); if (mScript != null) mScript.destroy(); + diag("DONE", "onDone complete"); + closeDiagLog(); } @Override protected void onInitRender() { + diag("SURFACE", "initRender width=%d height=%d runScript=%s", mWidth, mHeight, (mRunScript == null ? "" : mRunScript)); mTextureManager = new GLTextureManager(); mHelper = new GLHelper(mWidth, mHeight, GLHelper.getDefaultVMatrix()); mTextManager = new GLTextManager(mTextureManager, mHelper, mWidth, mHeight, mHeight / mLines); @@ -291,6 +580,7 @@ protected void onInitRender() { if (mLogcat != null) mLogcat.setReady(); } else { mScript = new Script(this, mRunScript); + diag("SURFACE", "script renderer started for %s", mRunScript); } } @@ -316,6 +606,7 @@ public void onGLRenderFrame() { @Override protected void onDoneRender() { + diag("SURFACE", "doneRender"); mTextManager.destroy(); mTextManager = null; mTextureManager.destroy(); @@ -330,6 +621,7 @@ public void onLine(Object sender, String text, int color) { mHandler.post(new Runnable() { @Override public void run() { + maybeLogFirstLine(s, t); if (mTextManager != null) { long wait = 0L; if (mComplete == 0) { @@ -352,6 +644,11 @@ public void run() { } else { mTextManager.add("", Color.WHITE, mWordWrap); } + } else { + mDroppedLines++; + if ((mDroppedLines <= 5) || ((mDroppedLines % 100) == 0)) { + diag("LINES", "drop count=%d sender=%s text=%s", mDroppedLines, getSenderName(s), shorten(t)); + } } } }); @@ -377,60 +674,60 @@ public void onLog(Object sender, String text) { @Override protected void onMainLoop() { if (mTest) { + diag("BOOT", "test mode sleep=%d", TEST_TIME); try { Thread.sleep(TEST_TIME); } catch (Exception e) { + diagException("BOOT", e); } } else { long start = SystemClock.elapsedRealtime(); boolean bootAnimationSeen = false; boolean bootAnimationGone = false; - boolean bootAnimationKilled = false; long complete = 0; + diag("BOOT", "main loop start"); while (true) { - // do not check sys.boot_completed or dev.bootcomplete, as these are already set to 1 before entering decryption password long now = SystemClock.elapsedRealtime(); - if ((complete == 0) && SystemProperties.get("service.bootanim.exit", "0").equals("1")) { - // sign from Android that we should quit - Logger.d("service.bootanim.exit"); - complete = now; - } - if ((complete == 0) && SystemProperties.get("service.bootanim.completed", "0").equals("1")) { - // sign from Android that we should quit - Logger.d("service.bootanim.completed"); + long elapsed = now - start; + List bootAnimationPids = getBootAnimationPids(); + boolean bootAnimationRunning = isBootAnimationRunning(bootAnimationPids); + String completeSignal = getBootCompletionSignal(bootAnimationSeen, bootAnimationRunning, elapsed); + maybeLogBootState(elapsed, bootAnimationSeen, bootAnimationGone, bootAnimationRunning, bootAnimationPids, complete, completeSignal); + if ((complete == 0) && (completeSignal != null)) { + Logger.d(completeSignal); + diag("BOOT", "complete signal=%s", completeSignal); complete = now; } - if ((complete == 0) && !bootAnimationSeen && (now - start > 1500)) { + if ((complete == 0) && !bootAnimationSeen && (elapsed > 1500)) { // register if we ever saw the bootanimation - if (isBootAnimationRunning()) { + if (bootAnimationRunning) { Logger.d("bootAnimationSeen"); + diag("BOOT", "bootAnimationSeen pids=%s", bootAnimationPids.toString()); bootAnimationSeen = true; } } - if (bootAnimationSeen && !bootAnimationGone && (now - start > 2500)) { + if (bootAnimationSeen && !bootAnimationGone && (elapsed > 2500)) { // if we saw the bootanimation before and its gone now, note that - if (!isBootAnimationRunning()) { + if (!bootAnimationRunning) { Logger.d("bootAnimationGone"); - bootAnimationGone = true; - } - } - if ((complete > 0) && bootAnimationSeen && !bootAnimationGone && !bootAnimationKilled) { - // if we have an exit sign from Android and bootanimation is still running, kill it - Logger.d("bootAnimationkill"); - killBootAnimation(); - bootAnimationKilled = true; - if (!isBootAnimationRunning()) { - Logger.d("bootAnimationkill/Gone"); + diag("BOOT", "bootAnimationGone"); bootAnimationGone = true; } } if ((complete == 0) && (new File(LIVEBOOT_ABORT_FILE)).exists()) { Logger.d("bootCompleteAbortFromAPK"); + diag("BOOT", "abort file detected: %s", LIVEBOOT_ABORT_FILE); complete = now; bootAnimationSeen = true; - bootAnimationKilled = true; bootAnimationGone = true; } + if ((complete == 0) && (elapsed > FAILSAFE_MAX_RUNTIME)) { + Logger.d("bootAnimationTimeout"); + diag("BOOT", "timeout after %d ms", elapsed); + complete = now; + bootAnimationSeen = true; + bootAnimationGone = true; + } if ( // Android has signaled to quit, and we haven't seen bootanimation @@ -443,10 +740,20 @@ protected void onMainLoop() { (bootAnimationSeen && (complete > 0) && (now - complete > 2500)) ) { Logger.dp("EXIT", "exit sequence"); + diag("BOOT", "exit sequence entered"); if ((mRunScript != null) && !mTest) { try { Thread.sleep(FOLLOW_TIME_SCRIPT); } catch (Exception e) { + diagException("BOOT", e); + } + } + if (mSuicideDelayMs > 0) { + diag("BOOT", "suicide delay sleep=%d", mSuicideDelayMs); + try { + Thread.sleep(mSuicideDelayMs); + } catch (Exception e) { + diagException("BOOT", e); } } break; @@ -454,9 +761,11 @@ protected void onMainLoop() { try { Thread.sleep(64); } catch (Exception e) { + diagException("BOOT", e); } } Logger.d("Runtime: %dms", SystemClock.elapsedRealtime() - start); + diag("BOOT", "runtime=%d", SystemClock.elapsedRealtime() - start); } mComplete = SystemClock.elapsedRealtime(); mLinesPassed = 0; @@ -466,6 +775,7 @@ protected void onMainLoop() { try { Thread.sleep(LEAD_TIME); } catch (Exception e) { + diagException("BOOT", e); } if (mLogSave) { mLogLock.lock(); @@ -479,7 +789,7 @@ protected void onMainLoop() { mLogLock.unlock(); } } - killBootAnimation(); + diag("DONE", "final cleanup start"); infanticide(); suicide(); } diff --git a/liveBootAni2/src/main/res/values/strings.xml b/liveBootAni2/src/main/res/values/strings.xml index e692b29..6cd3413 100644 --- a/liveBootAni2/src/main/res/values/strings.xml +++ b/liveBootAni2/src/main/res/values/strings.xml @@ -60,6 +60,8 @@ Use a darker background than the default setting Lines Number of lines to fit on screen + Exit delay + Wait this many milliseconds before LiveBoot exits after boot is considered complete. Useful to cover black gaps after the stock boot animation disappears. 0 disables. Word wrap If a line doesn\'t fit on screen, break it up into multiple lines