BackgroundRunner (#3008)

* Use getCapacityHW() to ensure latest battery capacity can be retrieved

* BackgroundRunner

* Start background_runner_spec.lua

* AutofrontLight plugin now uses BackgroundRunner plugin
pull/3034/head
Hzj_jie 7 years ago committed by Frans de Jonge
parent ba96506483
commit c9a997f42c

@ -8,6 +8,7 @@ end
local ConfirmBox = require("ui/widget/confirmbox")
local DataStorage = require("datastorage")
local LuaSettings = require("luasettings")
local PluginShare = require("pluginshare")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
@ -21,27 +22,34 @@ local AutoFrontlight = {
last_brightness = -1,
}
PluginShare.backgroundJobs = PluginShare.backgroundJobs or {}
function AutoFrontlight:_schedule(settings_id)
if not self.enabled then
logger.dbg("AutoFrontlight:_schedule() is disabled")
return
end
if settings_id ~= self.settings_id then
logger.dbg("AutoFrontlight:_schedule(): registered settings_id ",
settings_id,
" does not equal to current one ",
self.settings_id)
return
local enabled = function()
if not self.enabled then
logger.dbg("AutoFrontlight:_schedule() is disabled")
return false
end
if settings_id ~= self.settings_id then
logger.dbg("AutoFrontlight:_schedule(): registered settings_id ",
settings_id,
" does not equal to current one ",
self.settings_id)
return false
end
return true
end
logger.dbg("AutoFrontlight:_schedule() starts with settings_id ", settings_id)
self:_action()
logger.dbg("AutoFrontlight:_schedule() @ ", os.time(), ", it should be executed at ", os.time() + 2)
UIManager:scheduleIn(2, function()
self:_schedule(settings_id)
end)
table.insert(PluginShare.backgroundJobs, {
when = 2,
repeated = enabled,
executable = function()
if enabled() then
self:_action()
end
end
})
end
function AutoFrontlight:_action()

@ -0,0 +1,90 @@
local logger = require("logger")
local CommandRunner = {
pio = nil,
job = nil,
}
function CommandRunner:createEnvironmentFromTable(t)
if t == nil then return "" end
local r = ""
for k, v in pairs(t) do
r = r .. k .. "=" .. v .. " "
end
if string.len(r) > 0 then r = "export " .. r .. ";" end
return r
end
function CommandRunner:createEnvironment()
if type(self.job.environment) == "table" then
return self:createEnvironmentFromTable(self.job.environment)
end
if type(self.job.environment) == "function" then
local status, result = pcall(self.job.environment)
if status then
return self:createEnvironmentFromTable(result)
end
end
return ""
end
function CommandRunner:start(job)
assert(self ~= nil)
assert(self.pio == nil)
assert(self.job == nil)
self.job = job
self.job.start_sec = os.time()
assert(type(self.job.executable) == "string")
local command = self:createEnvironment() .. " " ..
"sh plugins/backgroundrunner.koplugin/luawrapper.sh " ..
"\"" .. self.job.executable .. "\""
logger.dbg("CommandRunner: Will execute command " .. command)
self.pio = io.popen(command)
end
--- Polls the status of self.pio.
-- @return a table contains the result from luawrapper.sh. Returns nil if the
-- command has not been finished.
function CommandRunner:poll()
assert(self ~= nil)
assert(self.pio ~= nil)
assert(self.job ~= nil)
local line = self.pio:read()
if line == "" then
return nil
else
if line == nil then
-- The binary crashes without output. This should not happen.
self.job.result = 223
else
line = line .. self.pio:read("*a")
logger.dbg("CommandRunner: Receive output " .. line)
local status, result = pcall(loadstring(line))
if status and result ~= nil then
for k, v in pairs(result) do
self.job[k] = v
end
else
-- The output from binary is invalid.
self.job.result = 222
end
end
self.pio:close()
self.pio = nil
self.job.end_sec = os.time()
local job = self.job
self.job = nil
return job
end
end
--- Whether this is a running job.
-- @treturn boolean
function CommandRunner:pending()
assert(self ~= nil)
return self.pio ~= nil
end
return CommandRunner

@ -0,0 +1,35 @@
#!/bin/sh
# Converts the return of "sh wrapper.sh $@" into Lua format.
CURRENT_DIR=$(dirname "$0")
sh "$CURRENT_DIR/wrapper.sh" "$@" >/dev/null 2>&1 &
JOB_ID=$!
while true; do
if ps -p $JOB_ID >/dev/null 2>&1; then
# Unblock f:read().
echo
else
wait $JOB_ID
EXIT_CODE=$?
if [ "$EXIT_CODE" -eq "255" ]; then
TIMEOUT="true"
else
TIMEOUT="false"
fi
if [ "$EXIT_CODE" -eq "127" ]; then
BADCOMMAND="true"
else
BADCOMMAND="false"
fi
echo "return { \
result = $EXIT_CODE, \
timeout = $TIMEOUT, \
bad_command = $BADCOMMAND, \
}"
exit 0
fi
done

@ -0,0 +1,234 @@
local CommandRunner = require("commandrunner")
local PluginShare = require("pluginshare")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
-- BackgroundRunner is an experimental feature to execute non-critical jobs in
-- 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 repeated the job once it finished.
-- function: if the return value of the function is true, repeated the job
-- once it finished. 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_sec: number, the os.time() when the job was started.
-- end_sec: number, the os.time() when the job was stopped.
-- insert_sec: number, the os.time() when the job was inserted into queue.
PluginShare.backgroundJobs = PluginShare.backgroundJobs or {}
local BackgroundRunner = {
jobs = PluginShare.backgroundJobs,
}
--- 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)
assert(self ~= nil)
if type(job.executable) == "function" then
job.timeout = ((job.end_sec - job.start_sec) > 1)
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_sec = os.time()
local status, err = pcall(job.executable)
if status then
job.result = 0
else
job.result = 1
job.exception = err
end
job.end_sec = os.time()
self:_finishJob(job)
return true
else
return false
end
end
--- Polls the status of the pending CommandRunner.
function BackgroundRunner:_poll()
assert(self ~= nil)
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())
assert(self ~= nil)
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_sec == nil then
-- Jobs are first inserted to jobs table from external users. So
-- they may not have insert_sec field.
job.insert_sec = os.time()
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 = ((os.time() - job.insert_sec) >= 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
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
if PluginShare.stopBackgroundRunner == nil then
self:_schedule()
end
end
function BackgroundRunner:_schedule()
assert(self ~= nil)
UIManager:scheduleIn(2, function() self:_execute() end)
end
function BackgroundRunner:_insert(job)
assert(self ~= nil)
job.insert_sec = os.time()
table.insert(self.jobs, job)
end
BackgroundRunner:_schedule()
local BackgroundRunnerWidget = WidgetContainer:new{
name = "backgroundrunner",
runner = BackgroundRunner,
}
return BackgroundRunnerWidget

@ -0,0 +1,39 @@
#!/bin/sh
# Starts the arguments as a bash command in background with low priorty. The
# command will be killed if it executes for over 1 hour. If the command failed
# to start, this script returns 127. If the command is timed out, this script
# returns 255. Otherwise the return value of the command will be returned.
echo "TIMEOUT in environment: $TIMEOUT"
if [ -z "$TIMEOUT" ]; then
TIMEOUT=3600
fi
echo "Timeout has been set to $TIMEOUT seconds"
echo "Will start command $*"
echo "$@" | nice -n 19 sh &
JOB_ID=$!
echo "Job id: $JOB_ID"
for i in $(seq 1 1 $TIMEOUT); do
if ps -p $JOB_ID >/dev/null 2>&1; then
# Job is still running.
sleep 1
ROUND=$(printf "%s" "$i" | tail -c 1)
if [ "$ROUND" -eq "0" ]; then
echo "Job $JOB_ID is still running ... waited for $i seconds."
fi
else
wait $JOB_ID
exit $?
fi
done
echo "Command $* has timed out"
kill -9 $JOB_ID
exit 255

@ -1,5 +1,5 @@
describe("AutoFrontlight widget tests", function()
local Device, PowerD, MockTime, AutoFrontlight
local Device, PowerD, MockTime, class, AutoFrontlight, UIManager
setup(function()
require("commonrequire")
@ -41,20 +41,28 @@ describe("AutoFrontlight widget tests", function()
open(require("datastorage"):getSettingsDir() .. "/autofrontlight.lua"):
saveSetting("enable", "true"):
close()
require("ui/uimanager")._run_forever = true
UIManager = require("ui/uimanager")
UIManager._run_forever = true
requireBackgroundRunner()
class = dofile("plugins/autofrontlight.koplugin/main.lua")
-- Ensure the background runner has succeeded set the job.insert_sec.
MockTime:increase(2)
UIManager:handleInput()
end)
after_each(function()
AutoFrontlight:deprecateLastTask()
-- Ensure the scheduled task from this test case won't impact others.
MockTime:increase(2)
require("ui/uimanager"):handleInput()
UIManager:handleInput()
AutoFrontlight = nil
stopBackgroundRunner()
end)
it("should automatically turn on or off frontlight", function()
local UIManager = require("ui/uimanager")
local class = dofile("plugins/autofrontlight.koplugin/main.lua")
AutoFrontlight = class:new()
Device.brightness = 3
MockTime:increase(2)
@ -99,8 +107,6 @@ describe("AutoFrontlight widget tests", function()
end)
it("should turn on frontlight at the begining", function()
local UIManager = require("ui/uimanager")
local class = dofile("plugins/autofrontlight.koplugin/main.lua")
Device.brightness = 0
AutoFrontlight = class:new()
MockTime:increase(2)
@ -109,8 +115,6 @@ describe("AutoFrontlight widget tests", function()
end)
it("should turn off frontlight at the begining", function()
local UIManager = require("ui/uimanager")
local class = dofile("plugins/autofrontlight.koplugin/main.lua")
Device.brightness = 3
AutoFrontlight = class:new()
MockTime:increase(2)
@ -119,8 +123,6 @@ describe("AutoFrontlight widget tests", function()
end)
it("should handle configuration update", function()
local UIManager = require("ui/uimanager")
local class = dofile("plugins/autofrontlight.koplugin/main.lua")
Device.brightness = 0
AutoFrontlight = class:new()
MockTime:increase(2)

@ -0,0 +1,292 @@
describe("BackgroundRunner widget tests", function()
local Device, PluginShare, MockTime, UIManager
setup(function()
require("commonrequire")
package.unloadAll()
-- Device needs to be loaded before UIManager.
Device = require("device")
Device.input.waitEvent = function() end
PluginShare = require("pluginshare")
MockTime = require("mock_time")
MockTime:install()
UIManager = require("ui/uimanager")
UIManager._run_forever = true
requireBackgroundRunner()
end)
teardown(function()
MockTime:uninstall()
package.unloadAll()
stopBackgroundRunner()
end)
it("should start job", function()
local executed = false
table.insert(PluginShare.backgroundJobs, {
when = 10,
executable = function()
executed = true
end,
})
MockTime:increase(2)
UIManager:handleInput()
MockTime:increase(9)
UIManager:handleInput()
assert.is_false(executed)
MockTime:increase(2)
UIManager:handleInput()
assert.is_true(executed)
end)
it("should repeat job", function()
local executed = 0
table.insert(PluginShare.backgroundJobs, {
when = 1,
repeated = function() return executed < 10 end,
executable = function()
executed = executed + 1
end,
})
MockTime:increase(2)
UIManager:handleInput()
for i = 1, 10 do
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(i, executed)
end
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(10, executed)
end)
it("should repeat job for predefined times", function()
local executed = 0
table.insert(PluginShare.backgroundJobs, {
when = 1,
repeated = 10,
executable = function()
executed = executed + 1
end,
})
MockTime:increase(2)
UIManager:handleInput()
for i = 1, 10 do
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(i, executed)
end
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(10, executed)
end)
it("should block long job", function()
local executed = 0
local job = {
when = 1,
repeated = true,
executable = function()
executed = executed + 1
MockTime:increase(2)
end,
}
table.insert(PluginShare.backgroundJobs, job)
MockTime:increase(2)
UIManager:handleInput()
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(1, executed)
assert.is_true(job.timeout)
assert.is_true(job.blocked)
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(1, executed)
end)
it("should execute binary", function()
local executed = false
local job = {
when = 1,
executable = "ls | grep this-should-not-be-a-file",
callback = function()
executed = true
end,
}
table.insert(PluginShare.backgroundJobs, job)
while job.end_sec == nil do
MockTime:increase(2)
UIManager:handleInput()
end
-- grep should return 1 when there is no match.
assert.are.equal(1, job.result)
assert.is_false(job.timeout)
assert.is_false(job.bad_command)
assert.is_true(executed)
end)
it("should forward string environment to the executable", function()
local job = {
when = 1,
repeated = false,
executable = "echo $ENV1 | grep $ENV2",
environment = {
ENV1 = "yes",
ENV2 = "yes",
}
}
table.insert(PluginShare.backgroundJobs, job)
while job.end_sec == nil do
MockTime:increase(2)
UIManager:handleInput()
end
-- grep should return 0 when there is a match.
assert.are.equal(0, job.result)
assert.is_false(job.timeout)
assert.is_false(job.bad_command)
job.environment = {
ENV1 = "yes",
ENV2 = "no",
}
job.end_sec = nil
table.insert(PluginShare.backgroundJobs, job)
while job.end_sec == nil do
MockTime:increase(2)
UIManager:handleInput()
end
-- grep should return 1 when there is no match.
assert.are.equal(1, job.result)
assert.is_false(job.timeout)
assert.is_false(job.bad_command)
assert.are.not_equal(os.getenv("ENV1"), "yes")
assert.are.not_equal(os.getenv("ENV2"), "yes")
assert.are.not_equal(os.getenv("ENV2"), "no")
end)
it("should forward function environment to the executable", function()
local env2 = "yes"
local job = {
when = 1,
repeated = false,
executable = "echo $ENV1 | grep $ENV2",
environment = function()
return {
ENV1 = "yes",
ENV2 = env2,
}
end,
}
table.insert(PluginShare.backgroundJobs, job)
while job.end_sec == nil do
MockTime:increase(2)
UIManager:handleInput()
end
-- grep should return 0 when there is a match.
assert.are.equal(0, job.result)
assert.is_false(job.timeout)
assert.is_false(job.bad_command)
job.end_sec = nil
env2 = "no"
table.insert(PluginShare.backgroundJobs, job)
while job.end_sec == nil do
MockTime:increase(2)
UIManager:handleInput()
end
-- grep should return 1 when there is no match.
assert.are.equal(1, job.result)
assert.is_false(job.timeout)
assert.is_false(job.bad_command)
end)
it("should block long binary job", function()
local executed = 0
local job = {
when = 1,
repeated = true,
executable = "sleep 1h",
environment = {
TIMEOUT = 1
}
}
table.insert(PluginShare.backgroundJobs, job)
while job.end_sec == nil do
MockTime:increase(2)
UIManager:handleInput()
end
assert.are.equal(255, job.result)
assert.is_true(job.timeout)
assert.is_true(job.blocked)
end)
it("should execute callback", function()
local executed = 0
table.insert(PluginShare.backgroundJobs, {
when = 1,
repeated = 10,
executable = function() end,
callback = function()
executed = executed + 1
end,
})
MockTime:increase(2)
UIManager:handleInput()
for i = 1, 10 do
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(i, executed)
end
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(10, executed)
end)
it("should not execute two jobs sequentially", function()
local executed = 0
table.insert(PluginShare.backgroundJobs, {
when = 1,
executable = function()
executed = executed + 1
end,
})
table.insert(PluginShare.backgroundJobs, {
when = 1,
executable = function()
executed = executed + 1
end,
})
MockTime:increase(2)
UIManager:handleInput()
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(1, executed)
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(2, executed)
MockTime:increase(2)
UIManager:handleInput()
assert.are.equal(2, executed)
end)
end)

@ -97,3 +97,20 @@ package.unloadAll = function()
end
return #pending
end
local background_runner
requireBackgroundRunner = function()
require("pluginshare").stopBackgroundRunner = nil
if background_runner == nil then
local package_path = package.path
package.path = "plugins/backgroundrunner.koplugin/?.lua;" .. package.path
background_runner = dofile("plugins/backgroundrunner.koplugin/main.lua")
package.path = package_path
end
return background_runner
end
stopBackgroundRunner = function()
background_runner = nil
require("pluginshare").stopBackgroundRunner = true
end

Loading…
Cancel
Save