From 41e78b6ed3d11021f7265f452dac60766c6be8ca Mon Sep 17 00:00:00 2001 From: zwim <36999612+zwim@users.noreply.github.com> Date: Sat, 25 Jun 2022 22:46:43 +0200 Subject: [PATCH] userpatch: allow monkey-patching KOReader (#9104) Supersede old android-only patch.lua. --- Makefile | 4 +- frontend/device/android/device.lua | 2 +- frontend/ui/data/onetime_migration.lua | 19 ++++- frontend/userpatch.lua | 108 +++++++++++++++++++++++++ platform/android/llapp_main.lua | 48 ----------- reader.lua | 24 +++++- 6 files changed, 149 insertions(+), 56 deletions(-) create mode 100644 frontend/userpatch.lua diff --git a/Makefile b/Makefile index d1168fb89..357bad54f 100644 --- a/Makefile +++ b/Makefile @@ -103,9 +103,9 @@ endif ifdef ANDROID cd $(INSTALL_DIR)/koreader && \ ln -sf ../../$(ANDROID_DIR)/*.lua . - @echo "[*] Install afterupdate marker" - @echo "# If this file is here, there are no afterupdate scripts in /sdcard/koreader/scripts/afterupdate." > $(INSTALL_DIR)/koreader/afterupdate.marker endif + @echo "[*] Install update once marker" + @echo "# This file indicates that update once patches have not been applied yet." > $(INSTALL_DIR)/koreader/update_once.marker ifdef WIN32 @echo "[*] Install runtime libraries for win32..." cd $(INSTALL_DIR)/koreader && cp ../../$(WIN32_DIR)/*.dll . diff --git a/frontend/device/android/device.lua b/frontend/device/android/device.lua index 5f6698211..d86a629c6 100644 --- a/frontend/device/android/device.lua +++ b/frontend/device/android/device.lua @@ -460,7 +460,7 @@ end function Device:canExecuteScript(file) local file_ext = string.lower(util.getFileNameSuffix(file)) - if android.prop.flavor ~= "fdroid" and file_ext == "sh" then + if android.prop.flavor ~= "fdroid" and file_ext == "sh" then return true end end diff --git a/frontend/ui/data/onetime_migration.lua b/frontend/ui/data/onetime_migration.lua index 2d9d82f2d..3d129ff08 100644 --- a/frontend/ui/data/onetime_migration.lua +++ b/frontend/ui/data/onetime_migration.lua @@ -7,7 +7,7 @@ local lfs = require("libs/libkoreader-lfs") local logger = require("logger") -- Date at which the last migration snippet was added -local CURRENT_MIGRATION_DATE = 20220523 +local CURRENT_MIGRATION_DATE = 20220625 -- Retrieve the date of the previous migration, if any local last_migration_date = G_reader_settings:readSetting("last_migration_date", 0) @@ -400,5 +400,22 @@ if last_migration_date < 20220523 then migrateSettingsName("highlight_long_hold_threshold", "highlight_long_hold_threshold_s") end +-- #9104 +if last_migration_date < 20220625 then + os.remove("afterupdate.marker") + + -- Move an existing `koreader/patch.lua` to `koreader/patches/1-patch.lua` (-> will be excuted in `early`) + local data_dir = DataStorage:getDataDir() + local patch_dir = data_dir .. "/patches" + if lfs.attributes(data_dir .. "/patch.lua", "mode") == "file" then + if lfs.attributes(patch_dir, "mode") == nil then + if not lfs.mkdir(patch_dir, "mode") then + logger.err("User patch error creating directory", patch_dir) + end + end + os.rename(data_dir .. "/patch.lua", patch_dir .. "/1-patch.lua") + end +end + -- We're done, store the current migration date G_reader_settings:saveSetting("last_migration_date", CURRENT_MIGRATION_DATE) diff --git a/frontend/userpatch.lua b/frontend/userpatch.lua new file mode 100644 index 000000000..996db8333 --- /dev/null +++ b/frontend/userpatch.lua @@ -0,0 +1,108 @@ +--[[-- +Allows applying developer patches while running KOReader. + +The contents in `koreader/patches/` are applied on calling `userpatch.applyPatches(priority)`. +--]]-- + +local isAndroid, android = pcall(require, "android") + +local userpatch = { + -- priorities for user patches, + early_once = "0", -- to be started early on startup (once after an update) + early = "1", -- to be started early on startup (always, but after an `early_once`) + late = "2", -- to be started after UIManager is ready (always) + -- 3-7 are reserved for later use + before_exit = "8", -- to be started a bit before exit before settings are saved (always) + on_exit = "9", -- to be started right before exit (always) + + -- the patch function itself + applyPatches = function(priority) end, -- to be overwritten, if the device allows it. +} + +if isAndroid and android.prop.flavor == "fdroid" then + return userpatch -- allows to use applyPatches as a no-op on F-Droid flavor +end + +local lfs = require("libs/libkoreader-lfs") +local logger = require("logger") +local DataStorage = require("datastorage") + +-- the directory KOReader is installed in (and runs from) +local package_dir = lfs.currentdir() +-- the directory where KOReader stores user data +local data_dir = DataStorage:getDataDir() + +--- Run lua patches +-- Execution order order is alphanum-sort for humans version 4: `1-patch.lua` is executed before `10-patch.lua` +-- (see http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua) +-- string directory ... to scan through (flat no recursion) +-- string priority ... only files starting with `priority` followed by digits and a '-' will be processed. +-- return true if a patch was executed +local function runUserPatchTasks(dir, priority) + if lfs.attributes(dir, "mode") ~= "directory" then + return + end + + local patches = {} + for entry in lfs.dir(dir) do + local mode = lfs.attributes(dir .. "/" .. entry, "mode") + if entry and mode == "file" and entry:match("^" .. priority .. "%d*%-") then + table.insert(patches, entry) + end + end + + if #patches == 0 then + return -- nothing to do + end + + local function addLeadingZeroes(d) + local dec, n = string.match(d, "(%.?)0*(.+)") + return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) + end + local function sorting(a, b) + return tostring(a):gsub("%.?%d+", addLeadingZeroes)..("%3d"):format(#b) + < tostring(b):gsub("%.?%d+", addLeadingZeroes)..("%3d"):format(#a) + end + + table.sort(patches, sorting) + + for i, entry in ipairs(patches) do + local fullpath = dir .. "/" .. entry + if lfs.attributes(fullpath, "mode") == "file" then + if fullpath:match("%.lua$") then -- execute patch-files first + logger.info("Applying patch:", fullpath) + local ok, err = pcall(dofile, fullpath) + if not ok then + logger.warn("Patching failed:", err) + -- Only show InfoMessage, when UIManager is working + if priority >= userpatch.late and priority < userpatch.before_exit then + -- Only developers (advanced users) will use this mechanism. + -- A warning on a patch failure after an OTA update will simplify troubleshooting. + local UIManager = require("ui/uimanager") + local InfoMessage = require("ui/widget/infomessage") + UIManager:show(InfoMessage:new{text = "Error applying patch:\n" .. fullpath}) -- no translate + end + end + end + end + end + return true +end + +--- This function applies lua patches from `/koreader/patches` +---- @string priority ... one of the defined priorities in the userpatch hashtable +function userpatch.applyPatches(priority) + local patch_dir = data_dir .. "/patches" + local update_once_marker = package_dir .. "/update_once.marker" + local update_once_pending = lfs.attributes(update_once_marker, "mode") == "file" + + if priority >= userpatch.early or update_once_pending then + local executed_something = runUserPatchTasks(patch_dir, priority) + if executed_something and update_once_pending then + -- Only delete update once marker if `early_once` updates have been applied. + os.remove(update_once_marker) -- Prevent another execution on a further starts. + end + end +end + +return userpatch diff --git a/platform/android/llapp_main.lua b/platform/android/llapp_main.lua index 2ca770a49..1242f6f55 100644 --- a/platform/android/llapp_main.lua +++ b/platform/android/llapp_main.lua @@ -16,54 +16,6 @@ end -- path to primary external storage partition local path = android.getExternalStoragePath() --- run user shell scripts or recursive migration of user data -local function runUserScripts(dir, migration, parent) - for entry in lfs.dir(dir) do - if entry ~= "." and entry ~= ".." then - local fullpath = dir .. "/" .. entry - local mode = lfs.attributes(fullpath).mode - if mode == "file" and migration then - if entry ~= "migrate" and not fullpath:match(".sh$") then - local destdir = parent and android.dir .. "/" .. parent or android.dir - -- we cannot create new directories on asset storage. - -- trying to do that crashes the VM with error=13, Permission Denied - android.execute("cp", fullpath, destdir .."/".. entry) - end - elseif mode == "file" and fullpath:match(".sh$") then - android.execute("sh", fullpath, path .. "/koreader", android.dir) - elseif mode == "directory" then - runUserScripts(fullpath, migration, parent and parent .. "/" .. entry or entry) -- recurse into next directory - end - end - end -end - -if android.prop.runtimeChanges then - -- run scripts once after an update of koreader, - -- it can also trigger a recursive migration of user data - local run_once_scripts = path .. "/koreader/scripts.afterupdate" - if lfs.attributes(run_once_scripts, "mode") == "directory" then - local afterupdate_marker = android.dir .. "/afterupdate.marker" - if lfs.attributes(afterupdate_marker, "mode") ~= nil then - if lfs.attributes(run_once_scripts .. "/migrate", "mode") ~= nil then - android.LOGI("after-update: running migration") - runUserScripts(run_once_scripts, true) - else - android.LOGI("after-update: running shell scripts") - runUserScripts(run_once_scripts) - end - android.execute("rm", afterupdate_marker) - end - end - -- scripts executed every start of koreader, no migration here - local run_always_scripts = path .. "/koreader/scripts.always" - if lfs.attributes(run_always_scripts, "mode") == "directory" then - runUserScripts(run_always_scripts) - end - -- run koreader patch before koreader startup - pcall(dofile, path.."/koreader/patch.lua") -end - -- set TESSDATA_PREFIX env var C.setenv("TESSDATA_PREFIX", path.."/koreader/data", 1) diff --git a/reader.lua b/reader.lua index 07120700a..cf822c640 100755 --- a/reader.lua +++ b/reader.lua @@ -13,13 +13,19 @@ io.stdout:write([[ [*] Current time: ]], os.date("%x-%X"), "\n") io.stdout:flush() +-- Set up Lua and ffi search paths +require("setupkoenv") + +-- Apply startup user patches and execute startup user scripts +local userpatch = require("userpatch") +userpatch.applyPatches(userpatch.early_once) +userpatch.applyPatches(userpatch.early) + -- Load default settings require("defaults") local DataStorage = require("datastorage") pcall(dofile, DataStorage:getDataDir() .. "/defaults.persistent.lua") --- Set up Lua and ffi search paths -require("setupkoenv") io.stdout:write(" [*] Version: ", require("version"):getCurrentRevision(), "\n\n") io.stdout:flush() @@ -205,6 +211,9 @@ end local UIManager = require("ui/uimanager") +-- Apply developer patches +userpatch.applyPatches(userpatch.late) + -- Inform once about color rendering on newly supported devices -- (there are some android devices that may not have a color screen, -- and we are not (yet?) able to guess that fact) @@ -363,6 +372,13 @@ local function exitReader() end end -local ret = exitReader() +-- Apply before_exit patches and execute user scripts +userpatch.applyPatches(userpatch.before_exit) + +local reader_retval = exitReader() + +-- Apply exit user patches and execute user scripts +userpatch.applyPatches(userpatch.on_exit) + -- Close the Lua state on exit -os.exit(ret, true) +os.exit(reader_retval, true)