mirror of
https://github.com/koreader/koreader
synced 2024-11-02 15:40:16 +00:00
fadee1f5dc
Basically: * Use `extend` for class definitions * Use `new` for object instantiations That includes some minor code cleanups along the way: * Updated `Widget`'s docs to make the semantics clearer. * Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283) * Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass). * Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events. * Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier. * Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references. * ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak). * Terminal: Make sure the shell is killed on plugin teardown. * InputText: Fix Home/End/Del physical keys to behave sensibly. * InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...). * OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of. * ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed! * Kobo: Minor code cleanups.
280 lines
9.7 KiB
Lua
280 lines
9.7 KiB
Lua
local Device = require("device")
|
|
|
|
-- disable on android, since it breaks expect behaviour of an activity.
|
|
-- it is also unused by other plugins.
|
|
-- See https://github.com/koreader/koreader/issues/6297
|
|
if Device:isAndroid() then
|
|
return { disabled = true, }
|
|
end
|
|
|
|
local CommandRunner = require("commandrunner")
|
|
local PluginShare = require("pluginshare")
|
|
local UIManager = require("ui/uimanager")
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
local logger = require("logger")
|
|
local time = require("ui/time")
|
|
local _ = require("gettext")
|
|
|
|
-- BackgroundRunner is an experimental feature to execute non-critical jobs in
|
|
-- the background.
|
|
-- A job is defined as a table in PluginShare.backgroundJobs table.
|
|
-- It contains at least following items:
|
|
-- when: number, string or function
|
|
-- number: the delay in seconds
|
|
-- string: "best-effort" - the job will be started when there is no other jobs
|
|
-- to be executed.
|
|
-- "idle" - the job will be started when the device is idle.
|
|
-- function: if the return value of the function is true, the job will be
|
|
-- executed immediately.
|
|
--
|
|
-- repeated: boolean or function or nil or number
|
|
-- boolean: true to repeat the job once it finished.
|
|
-- function: if the return value of the function is true, repeat the job
|
|
-- once it finishes. If the function throws an error, it equals to
|
|
-- return false.
|
|
-- nil: same as false.
|
|
-- number: times to repeat.
|
|
--
|
|
-- executable: string or function
|
|
-- string: the command line to be executed. The command or binary will be
|
|
-- executed in the lowest priority. Command or binary will be killed
|
|
-- if it executes for over 1 hour.
|
|
-- function: the action to be executed. The execution cannot be killed, but it
|
|
-- will be considered as timeout if it executes for more than 1
|
|
-- second.
|
|
-- If the executable times out, the job will be blocked, i.e. the repeated
|
|
-- field will be ignored.
|
|
--
|
|
-- environment: table or function or nil
|
|
-- table: the key-value pairs of all environments set for string executable.
|
|
-- function: the function to return a table of environments.
|
|
-- nil: ignore.
|
|
--
|
|
-- callback: function or nil
|
|
-- function: the action to be executed when executable has been finished.
|
|
-- Errors thrown from this function will be ignored.
|
|
-- nil: ignore.
|
|
--
|
|
-- If a job does not contain enough information, it will be ignored.
|
|
--
|
|
-- Once the job is finished, several items will be added to the table:
|
|
-- result: number, the return value of the command. In general, 0 means
|
|
-- succeeded.
|
|
-- For function executable, 1 if the function throws an error.
|
|
-- For string executable, several predefined values indicate the
|
|
-- internal errors. E.g. 223: the binary crashes. 222: the output is
|
|
-- invalid. 127: the command is invalid. 255: the command timed out.
|
|
-- Typically, consumers can use following states instead of hardcodeing
|
|
-- the error codes.
|
|
-- exception: error, the error returned from function executable. Not available
|
|
-- for string executable.
|
|
-- timeout: boolean, whether the command times out.
|
|
-- bad_command: boolean, whether the command is not found. Not available for
|
|
-- function executable.
|
|
-- blocked: boolean, whether the job is blocked.
|
|
-- start_time: number, the time (fts) when the job was started.
|
|
-- end_time: number, the time (fts) when the job was stopped.
|
|
-- insert_time: number, the time (fts) when the job was inserted into queue.
|
|
-- (All of them in the monotonic time scale, like the main event loop & task queue).
|
|
|
|
local BackgroundRunner = {
|
|
jobs = PluginShare.backgroundJobs,
|
|
running = false,
|
|
}
|
|
|
|
--- Copies required fields from |job|.
|
|
-- @return a new table with required fields of a valid job.
|
|
function BackgroundRunner:_clone(job)
|
|
assert(job ~= nil)
|
|
local result = {}
|
|
result.when = job.when
|
|
result.repeated = job.repeated
|
|
result.executable = job.executable
|
|
result.callback = job.callback
|
|
result.environment = job.environment
|
|
return result
|
|
end
|
|
|
|
function BackgroundRunner:_shouldRepeat(job)
|
|
if type(job.repeated) == "nil" then return false end
|
|
if type(job.repeated) == "boolean" then return job.repeated end
|
|
if type(job.repeated) == "function" then
|
|
local status, result = pcall(job.repeated)
|
|
if status then
|
|
return result
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
if type(job.repeated) == "number" then
|
|
job.repeated = job.repeated - 1
|
|
return job.repeated > 0
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function BackgroundRunner:_finishJob(job)
|
|
if type(job.executable) == "function" then
|
|
local time_diff = job.end_time - job.start_time
|
|
local threshold = time.s(1)
|
|
job.timeout = (time_diff > threshold)
|
|
end
|
|
job.blocked = job.timeout
|
|
if not job.blocked and self:_shouldRepeat(job) then
|
|
self:_insert(self:_clone(job))
|
|
end
|
|
if type(job.callback) == "function" then
|
|
pcall(job.callback)
|
|
end
|
|
end
|
|
|
|
--- Executes |job|.
|
|
-- @treturn boolean true if job is valid.
|
|
function BackgroundRunner:_executeJob(job)
|
|
assert(not CommandRunner:pending())
|
|
if job == nil then return false end
|
|
if job.executable == nil then return false end
|
|
|
|
if type(job.executable) == "string" then
|
|
CommandRunner:start(job)
|
|
return true
|
|
elseif type(job.executable) == "function" then
|
|
job.start_time = UIManager:getTime()
|
|
local status, err = pcall(job.executable)
|
|
if status then
|
|
job.result = 0
|
|
else
|
|
job.result = 1
|
|
job.exception = err
|
|
end
|
|
job.end_time = time.now()
|
|
self:_finishJob(job)
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
--- Polls the status of the pending CommandRunner.
|
|
function BackgroundRunner:_poll()
|
|
assert(CommandRunner:pending())
|
|
local result = CommandRunner:poll()
|
|
if result == nil then return end
|
|
|
|
self:_finishJob(result)
|
|
end
|
|
|
|
function BackgroundRunner:_execute()
|
|
logger.dbg("BackgroundRunner: _execute() @ ", os.time())
|
|
if CommandRunner:pending() then
|
|
self:_poll()
|
|
else
|
|
local round = 0
|
|
while #self.jobs > 0 do
|
|
local job = table.remove(self.jobs, 1)
|
|
if job.insert_time == nil then
|
|
-- Jobs are first inserted to jobs table from external users.
|
|
-- So they may not have an insert field.
|
|
job.insert_time = UIManager:getTime()
|
|
end
|
|
local should_execute = false
|
|
local should_ignore = false
|
|
if type(job.when) == "function" then
|
|
local status, result = pcall(job.when)
|
|
if status then
|
|
should_execute = result
|
|
else
|
|
should_ignore = true
|
|
end
|
|
elseif type(job.when) == "number" then
|
|
if job.when >= 0 then
|
|
should_execute = (UIManager:getTime() - job.insert_time >= time.s(job.when))
|
|
else
|
|
should_ignore = true
|
|
end
|
|
elseif type(job.when) == "string" then
|
|
--- @todo (Hzj_jie): Implement "idle" mode
|
|
if job.when == "best-effort" then
|
|
should_execute = (round > 0)
|
|
elseif job.when == "idle" then
|
|
should_execute = (round > 1)
|
|
else
|
|
should_ignore = true
|
|
end
|
|
else
|
|
should_ignore = true
|
|
end
|
|
|
|
if should_execute then
|
|
logger.dbg("BackgroundRunner: run job ", job, " @ ", os.time())
|
|
assert(not should_ignore)
|
|
self:_executeJob(job)
|
|
break
|
|
elseif not should_ignore then
|
|
table.insert(self.jobs, job)
|
|
end
|
|
|
|
round = round + 1
|
|
if round > 2 then break end
|
|
end
|
|
end
|
|
|
|
self.running = false
|
|
if PluginShare.stopBackgroundRunner == nil then
|
|
if #self.jobs == 0 and not CommandRunner:pending() then
|
|
logger.dbg("BackgroundRunnerWidget: no job, stop running @ ", os.time())
|
|
else
|
|
self:_schedule()
|
|
end
|
|
else
|
|
logger.dbg("BackgroundRunnerWidget: stop running @ ", os.time())
|
|
end
|
|
end
|
|
|
|
function BackgroundRunner:_schedule()
|
|
if self.running == false then
|
|
if #self.jobs == 0 and not CommandRunner:pending() then
|
|
logger.dbg("BackgroundRunnerWidget: no job, not running @ ", os.time())
|
|
else
|
|
logger.dbg("BackgroundRunnerWidget: start running @ ", os.time())
|
|
self.running = true
|
|
UIManager:scheduleIn(2, function() self:_execute() end)
|
|
end
|
|
else
|
|
logger.dbg("BackgroundRunnerWidget: a schedule is pending @ ",
|
|
os.time())
|
|
end
|
|
end
|
|
|
|
function BackgroundRunner:_insert(job)
|
|
job.insert_time = UIManager:getTime()
|
|
table.insert(self.jobs, job)
|
|
end
|
|
|
|
BackgroundRunner:_schedule()
|
|
|
|
local BackgroundRunnerWidget = WidgetContainer:extend{
|
|
name = "backgroundrunner",
|
|
runner = BackgroundRunner,
|
|
}
|
|
|
|
function BackgroundRunnerWidget:onSuspend()
|
|
logger.dbg("BackgroundRunnerWidget:onSuspend() @ ", os.time())
|
|
PluginShare.stopBackgroundRunner = true
|
|
end
|
|
|
|
function BackgroundRunnerWidget:onResume()
|
|
logger.dbg("BackgroundRunnerWidget:onResume() @ ", os.time())
|
|
PluginShare.stopBackgroundRunner = nil
|
|
BackgroundRunner:_schedule()
|
|
end
|
|
|
|
function BackgroundRunnerWidget:onBackgroundJobsUpdated()
|
|
logger.dbg("BackgroundRunnerWidget:onBackgroundJobsUpdated() @ ", os.time())
|
|
PluginShare.stopBackgroundRunner = nil
|
|
BackgroundRunner:_schedule()
|
|
end
|
|
|
|
return BackgroundRunnerWidget
|