Commit 030eba98 authored by Karel Koci's avatar Karel Koci 🤘

Add support for package reboot extra field

parent 09e1a499
......@@ -78,6 +78,7 @@ enum option_val {
OPT_BATCH_VAL = 260,
OPT_STATE_LOG_VAL,
OPT_REEXEC_VAL,
OPT_REBOOT_VAL,
OPT_ASK_APPROVAL_VAL,
OPT_APPROVE_VAL,
OPT_OUTPUT,
......@@ -93,6 +94,7 @@ static const struct option opt_long[] = {
{ .name = "remove", .has_arg = required_argument, .val = 'r' },
{ .name = "batch", .has_arg = no_argument, .val = OPT_BATCH_VAL },
{ .name = "reexec", .has_arg = no_argument, .val = OPT_REEXEC_VAL },
{ .name = "reboot-finished", .has_arg = no_argument, .val = OPT_REBOOT_VAL },
{ .name = "state-log", .has_arg = no_argument, .val = OPT_STATE_LOG_VAL },
{ .name = "ask-approval", .has_arg = required_argument, .val = OPT_ASK_APPROVAL_VAL },
{ .name = "approve", .has_arg = required_argument, .val = OPT_APPROVE_VAL },
......@@ -112,6 +114,7 @@ static const struct simple_opt {
['e'] = { COT_STDERR_LEVEL, true, true },
[OPT_BATCH_VAL] = { COT_BATCH, false, true },
[OPT_REEXEC_VAL] = { COT_REEXEC, false, true },
[OPT_REBOOT_VAL] = { COT_REBOOT, false, true },
[OPT_STATE_LOG_VAL] = { COT_STATE_LOG, false, true },
[OPT_ASK_APPROVAL_VAL] = { COT_ASK_APPROVAL, true, true },
[OPT_APPROVE_VAL] = { COT_APPROVE, true, true },
......@@ -280,17 +283,18 @@ void arg_backup_clear() {
orig_wd = NULL;
}
void reexec() {
void reexec(int args_count, char *args[]) {
ASSERT_MSG(back_argv, "No arguments backed up");
// Try restoring the working directory to the original, but don't insist
if (orig_wd)
chdir(orig_wd);
// Extend back_argv by --reexec
char **argv;
argv = alloca((back_argc + 2) * sizeof *argv);
memcpy(argv, back_argv, back_argc * sizeof *back_argv);
argv[back_argc] = "--reexec";
argv[back_argc + 1] = NULL;
execvp(argv[0], argv);
DIE("Failed to reexec %s: %s", argv[0], strerror(errno));
// Extend back_argv by --reexec and additional arguments
char **new_argv;
new_argv = alloca((back_argc + args_count + 2) * sizeof *args);
memcpy(new_argv, back_argv, back_argc * sizeof *back_argv);
memcpy(new_argv + back_argc, args, args_count * sizeof *args);
new_argv[back_argc + args_count] = "--reexec";
new_argv[back_argc + args_count + 1] = NULL;
execvp(new_argv[0], new_argv);
DIE("Failed to reexec %s: %s", new_argv[0], strerror(errno));
}
......@@ -44,6 +44,8 @@ enum cmd_op_type {
COT_BATCH,
// Internally used when reexecuting. Informs program that this is restarted instance.
COT_REEXEC,
// Internally used when reexecuting to pass information that we should reboot after updater finishes.
COT_REBOOT,
// Enable dumping state to files in /tmp/updater-state directory
COT_STATE_LOG,
// Ask for approval of the operations by generating a report and requiring the --approve flag
......@@ -106,7 +108,12 @@ void arg_backup_clear();
/*
* Exec the same binary with the same arguments, effectively
* restarting the whole process.
* You can pass additional arguments that will be appended to end of original
* ones. Arguments args_count is number of arguments to be appended and args is
* array containing those arguments. You can pass (0, NULL) to append no
* arguments.
* This function newer returns so arguments can be allocated even on stack.
*/
void reexec() __attribute__((noreturn));
void reexec(int args_count, char *args[]) __attribute__((noreturn));
#endif
......@@ -831,6 +831,9 @@ Merge the given package into the live system and remove the temporary directory.
The configs parameter describes the previous version of the package, not
the current one.
Return value is boolean. False is returned if files were already merged and
true if files were merged in this function.
]]
function pkg_merge_files(dir, dirs, files, configs)
if stat(dir) == nil then
......@@ -840,7 +843,7 @@ function pkg_merge_files(dir, dirs, files, configs)
from journal), so skip it completely.
]]
DBG("Skipping installation of temporary dir " .. dir .. ", no longer present")
return
return false
end
--[[
First, create the needed directories. Sort them according to
......@@ -886,6 +889,7 @@ function pkg_merge_files(dir, dirs, files, configs)
end
-- Remove the original directory
utils.cleanup_dirs({dir})
return true
end
--[[
......
......@@ -43,10 +43,11 @@ local state_dump = state_dump
local sync = sync
local log_event = log_event
local sha256 = sha256
local system_reboot = system_reboot
module "transaction"
-- luacheck: globals perform recover empty perform_queue recover_pretty queue_remove queue_install queue_install_downloaded approval_hash task_report
-- luacheck: globals perform recover empty perform_queue recover_pretty queue_remove queue_install queue_install_downloaded approval_hash task_report cleanup_actions
-- Wrap the call to the maintainer script, and store any possible errors for later use
local function script(errors_collected, name, suffix, ...)
......@@ -74,6 +75,7 @@ local function pkg_unpack(operations, status)
local to_install = {}
-- Plan of the operations we have prepared, similar to operations, but with different things in them
local plan = {}
local cleanup_actions = {}
for _, op in ipairs(operations) do
if op.op == "remove" then
if status[op.name] then
......@@ -114,13 +116,20 @@ local function pkg_unpack(operations, status)
dirs = dirs,
configs = configs,
old_configs = old_configs,
control = control
control = control,
reboot_immediate = op.reboot == "immediate"
})
if op.replan then
cleanup_actions.reexec = true
end
if op.reboot == "finished" or (op.reboot == "delayed" and not cleanup_actions.reboot) then
cleanup_actions.reboot = op.reboot
end
else
error("Unknown operation " .. op.op)
end
end
return to_remove, to_install, plan, dir_cleanups
return to_remove, to_install, plan, dir_cleanups, cleanup_actions
end
local function pkg_collision_check(status, to_remove, to_install)
......@@ -169,8 +178,13 @@ local function pkg_move(status, plan, early_remove, errors_collected)
if early_remove[op.control.Package] then
backend.pkg_cleanup_files(early_remove[op.control.Package], all_configs)
end
backend.pkg_merge_files(op.dir .. "/data", op.dirs, op.files, op.old_configs)
local did_merge = backend.pkg_merge_files(op.dir .. "/data", op.dirs, op.files, op.old_configs)
status[op.control.Package] = op.control
if op.reboot_immediate and did_merge then -- we reboot only if we did merge, if files were already merged then we already rebooted.
-- We can't exit this function, so it could finish from journal after reboot. We stuck execution here.
-- Note: This causes reexecution of already executed preinst scripts.
system_reboot(true)
end
end
-- Ignore others, at least for now.
end
......@@ -216,6 +230,14 @@ local function pkg_cleanup(status)
backend.status_dump(status)
end
--[[
Set of actions updater should perform after transaction is done.
• reexec - if updater should be re-executed afterward.
• reboot - contains if system should be rebooted and when. Only possible values
are "finished" and "delayed". Where "finished" has precedence.
]]
cleanup_actions = {}
-- The internal part of perform, re-run on journal recover
-- The lock file is expected to be already acquired and is released at the end.
local function perform_internal(operations, journal_status, run_state)
......@@ -257,8 +279,9 @@ local function perform_internal(operations, journal_status, run_state)
backend.dir_ensure(created)
end
-- Look at what the current status looks like.
local to_remove, to_install, plan, new_dir_cleanups = step(journal.UNPACKED, pkg_unpack, true, operations, status)
dir_cleanups = new_dir_cleanups
local to_remove, to_install, plan
to_remove, to_install, plan, dir_cleanups, cleanup_actions = step(journal.UNPACKED, pkg_unpack, true, operations, status)
cleanup_actions = cleanup_actions or {} -- just to handle if journal contains no cleanup actions (journal from previous version)
-- Drop the operations. This way, if we are tail-called, then the package buffers may be garbage-collected
operations = nil
-- Check for collisions
......@@ -300,6 +323,7 @@ is a single table, with these keys:
• op: The operation to perform. It is one of:
- install
- remove
• reboot: If and when system should be rebooted when this package is installed.
• name: Name of the package, needed for remove.
• data: Buffer containing the necessary data. It is needed in the case
of install, when it contains the ipk package.
......@@ -403,8 +427,15 @@ function queue_install(filename)
end
end
function queue_install_downloaded(data, name, version)
table.insert(queue, {op = "install", data = data, name = name, version = version})
function queue_install_downloaded(data, name, version, modifier)
table.insert(queue, {
op = "install",
data = data,
name = name,
version = version,
reboot = modifier.reboot,
replan = modifier.replan
})
end
local function queued_tasks()
......
......@@ -36,9 +36,7 @@ local transaction = require "transaction"
module "updater"
-- luacheck: globals prepare cleanup required_pkgs
local cleanup_actions = {}
-- luacheck: globals prepare pre_cleanup cleanup required_pkgs
function required_pkgs(entrypoint)
-- Get the top-level script
......@@ -103,13 +101,10 @@ function prepare(entrypoint)
error(utils.exception("corruption", "The sha256 sum of " .. task.name .. " does not match"))
end
end
transaction.queue_install_downloaded(data, task.name, task.package.Version)
transaction.queue_install_downloaded(data, task.name, task.package.Version, task.modifier)
else
error(data)
end
if task.modifier.replan then
cleanup_actions.replan = true
end
elseif task.action == "remove" then
INFO("Queue removal of " .. task.name)
transaction.queue_remove(task.name)
......@@ -119,9 +114,27 @@ function prepare(entrypoint)
end
end
function cleanup(success)
if cleanup_actions.replan then
reexec()
-- Only cleanup actions that we want to give chance to program to react on
function pre_cleanup()
local reboot_delayed = false
local reboot_finished = false
if transaction.cleanup_actions.reboot == "delayed" then
WARN("Restart your device to apply all changes.")
reboot_delayed = true
elseif transaction.cleanup_actions.reboot == "finished" then
reboot_finished = true
end
return reboot_delayed, reboot_finished
end
-- Note: This function don't have to return
function cleanup(success, reboot_finished)
if transaction.cleanup_actions.replan then
if reboot_finished then
reexec('--reboot-finished')
else
reexec()
end
end
if success then
backend.flags_write(true)
......
......@@ -666,8 +666,14 @@ static int lua_sha256(lua_State *L) {
return 1;
}
static int lua_reexec(lua_State *L __attribute__((unused))) {
reexec();
static int lua_reexec(lua_State *L) {
size_t args_c = lua_gettop(L);
const char *args[args_c];
size_t i;
for (i = 0; i < args_c; i++) {
args[i] = luaL_checkstring(L, i + 1);
}
reexec(args_c, (char**) args);
return 0;
}
......@@ -688,6 +694,12 @@ static int lua_uri_internal_get(lua_State *L) {
return 1;
}
static int lua_system_reboot(lua_State *L) {
bool stick = lua_toboolean(L, 1);
system_reboot(stick);
return 0;
}
extern bool state_log_enabled; // defined in util.c
static int lua_state_log_enabled(lua_State *L) {
......@@ -725,7 +737,8 @@ static const struct injected_func injected_funcs[] = {
{ lua_md5, "md5" },
{ lua_sha256, "sha256" },
{ lua_reexec, "reexec" },
{ lua_uri_internal_get, "uri_internal_get" }
{ lua_uri_internal_get, "uri_internal_get" },
{ lua_system_reboot, "system_reboot" }
};
struct interpreter *interpreter_create(struct events *events, const struct file_index_element *uriinter) {
......
......@@ -246,12 +246,18 @@ setenv(name, value)::
Set the environment variable with the given name to the given value.
Errors in case of failure, otherwise returns nothing.
reexec()::
reexec(args,...)::
Try to run the program from the beginning, preserving the command
line arguments and working directory. It depends on being set up
properly at the start of the program.
As arguments you can specify additional arguments that will be appended
at the end of original ones.
uri_internal_get(name)::
Function to access embedded files using `internal:` uri. Argument `name`
is name of requested embedded file. Returns file content. If there is
no file under given name, error is raised.
system_reboot(stick)::
Reboots system. Argument `stick` if set to true results in that this
newer returns.
......@@ -27,6 +27,8 @@
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <signal.h>
#include <poll.h>
struct level_info {
const char *prefix;
......@@ -179,6 +181,20 @@ void exec_dir(struct events *events, const char *dir) {
free(namelist);
}
void system_reboot(bool stick) {
WARN("Performing system reboot.");
if (!fork()) {
ASSERT_MSG(execvp("reboot",(char*[]){NULL}), "Execution of reboot command failed");
}
if (stick) {
sigset_t sigmask;
sigfillset(&sigmask);
while (1) {
ppoll(NULL, 0, NULL, sigmask);
}
}
}
size_t printf_len(const char *msg, ...) {
va_list args;
va_start(args, msg);
......
......@@ -69,6 +69,9 @@ bool dump2file (const char *file, const char *text) __attribute__((nonnull,nonnu
// Executes all executable files in given directory
void exec_dir(struct events *events, const char *dir) __attribute__((nonnull));
// Reboot system. Argument stick signals if updater should stick or continue.
void system_reboot(bool stick);
// Compute the size needed (including \0) to format given message
size_t printf_len(const char *msg, ...) __attribute__((format(printf, 1, 2)));
// Like sprintf, but returs the string. Expects there's enough space.
......
......@@ -46,7 +46,7 @@ static bool results_interpret(struct interpreter *interpreter, size_t result_cou
}
static const enum cmd_op_type cmd_op_allows[] = {
COT_BATCH, COT_NO_OP, COT_REEXEC, COT_STATE_LOG, COT_ROOT_DIR, COT_SYSLOG_LEVEL, COT_STDERR_LEVEL, COT_SYSLOG_NAME, COT_ASK_APPROVAL, COT_APPROVE, COT_TASK_LOG, COT_LAST
COT_BATCH, COT_NO_OP, COT_REEXEC, COT_REBOOT, COT_STATE_LOG, COT_ROOT_DIR, COT_SYSLOG_LEVEL, COT_STDERR_LEVEL, COT_SYSLOG_NAME, COT_ASK_APPROVAL, COT_APPROVE, COT_TASK_LOG, COT_LAST
};
static void print_help() {
......@@ -56,6 +56,7 @@ static void print_help() {
const char *hook_preupdate = "/etc/updater/hook_preupdate";
const char *hook_postupdate = "/etc/updater/hook_postupdate";
const char *hook_reboot_delayed = "/etc/updater/hook_reboot_required";
static bool approved(struct interpreter *interpreter, const char *approval_file, const char **approvals, size_t approval_count) {
if (!approval_file)
......@@ -110,7 +111,7 @@ int main(int argc, char *argv[]) {
struct cmd_op *op = ops;
const char *top_level_config = "internal:entry_lua";
const char *root_dir = NULL;
bool batch = false, early_exit = false, replan = false;
bool batch = false, early_exit = false, replan = false, reboot_finished = false;
const char *approval_file = NULL;
const char **approvals = NULL;
size_t approval_count = 0;
......@@ -135,6 +136,9 @@ int main(int argc, char *argv[]) {
case COT_REEXEC:
replan = true;
break;
case COT_REBOOT:
reboot_finished = true;
break;
case COT_ROOT_DIR:
root_dir = op->parameter;
break;
......@@ -234,7 +238,16 @@ int main(int argc, char *argv[]) {
err = interpreter_call(interpreter, "transaction.perform_queue", &result_count, "");
ASSERT_MSG(!err, "%s", err);
trans_ok = results_interpret(interpreter, result_count);
err = interpreter_call(interpreter, "updater.cleanup", NULL, "b", trans_ok);
err = interpreter_call(interpreter, "updater.pre_cleanup", NULL, "");
ASSERT_MSG(!err, "%s", err);
bool reboot_delayed;
ASSERT(interpreter_collect_results(interpreter, "bb", &reboot_delayed, &reboot_finished) == -1);
if (reboot_delayed) {
INFO("Executing reboot_required hooks...");
const char *hook_path = aprintf("%s%s", root_dir, hook_reboot_delayed);
exec_dir(events, hook_path);
}
err = interpreter_call(interpreter, "updater.cleanup", NULL, "bb", trans_ok, reboot_finished);
ASSERT_MSG(!err, "%s", err);
if (task_log) {
FILE *log = fopen(task_log, "a");
......@@ -253,6 +266,8 @@ CLEANUP:
interpreter_destroy(interpreter);
events_destroy(events);
arg_backup_clear();
if (reboot_finished)
system_reboot(false);
if (exit_type == COT_EXIT) {
if (trans_ok) {
state_dump("done");
......
......@@ -8,7 +8,7 @@ globals = {
-- lua51 doesn't contains?
"_M",
-- From interpreter.c
"log", "state_log_enabled", "state_dump", "run_command", "download", "events_wait", "mkdtemp", "chdir", "getcwd", "mkdir", "move", "ls", "stat", "lstat", "sync", "setenv", "md5", "sha256", "reexec", "uri_internal_get",
"log", "state_log_enabled", "state_dump", "run_command", "download", "events_wait", "mkdtemp", "chdir", "getcwd", "mkdir", "move", "ls", "stat", "lstat", "sync", "setenv", "md5", "sha256", "reexec", "uri_internal_get", "system_reboot",
-- From logging
"ERROR", "WARN", "INFO", "DBG", "DIE", "log_event", "c_pcall_error_handler",
-- From dumper
......
......@@ -46,12 +46,12 @@ function test_package()
tp = "package",
name = "pkg_name"
}, p1)
local p2 = run_sandbox_fun "Package 'pkg_name' {replan = true, reboot = true}"
local p2 = run_sandbox_fun "Package 'pkg_name' {replan = true, reboot = 'delayed'}"
assert_table_equal({
tp = "package",
name = "pkg_name",
replan = true,
reboot = true
reboot = "delayed"
}, p2)
assert_table_equal({p1, p2}, requests.known_packages)
end
......
......@@ -163,7 +163,7 @@ function test_perform_empty()
local expected = tables_join(intro, {
{
f = "journal.write",
p = {journal.UNPACKED, {}, {}, {}, {}}
p = {journal.UNPACKED, {}, {}, {}, {}, {}}
},
{
f = "backend.collision_check",
......@@ -246,11 +246,13 @@ function test_perform_ok()
dirs = { d = true },
files = { f = true },
op = "install",
old_configs = { c = "12345678901234567890123456789012" }
old_configs = { c = "12345678901234567890123456789012" },
reboot_immediate = false
},
{ name = "pkg-rem", op = "remove" }
},
{"pkg_dir"}
{"pkg_dir"},
{}
}
},
{
......@@ -342,7 +344,7 @@ function test_perform_ok()
assert_table_equal(expected, mocks_called)
end
-- Test it stops when it finds collisions
-- Test if it stops when it finds collisions
function test_perform_collision()
mocks_install()
mock_gen("backend.collision_check", function () return {f = {["<pkg1name>"] = "new", ["<pkg2name>"] = "new", ["other"] = "existing"}}, {} end)
......@@ -393,7 +395,8 @@ function test_perform_collision()
dirs = { d = true },
files = { f = true },
op = "install",
old_configs = { c = "1234567890123456" }
old_configs = { c = "1234567890123456" },
reboot_immediate = false
},
{
configs = { c = "1234567890123456" },
......@@ -402,10 +405,12 @@ function test_perform_collision()
dirs = { d = true },
files = { f = true },
op = "install",
old_configs = { c = "1234567890123456" }
old_configs = { c = "1234567890123456" },
reboot_immediate = false
}
},
{"<pkg1dir>", "<pkg2dir>"}
{"<pkg1dir>", "<pkg2dir>"},
{}
}
},
{
......@@ -495,7 +500,8 @@ function test_recover_late()
},
{ name = "pkg-rem", op = "remove" }
},
{"pkg_dir"}
{"pkg_dir"},
{}
} },
{ type = journal.CHECKED, params = { {["d2"] = true} } },
{ type = journal.MOVED, params = {
......@@ -573,47 +579,47 @@ function test_approval_hash()
-- The same lists of operations return the same hash
assert_true(equal(
{
{'install_downloaded', '', 'pkg', 13},
{'install_downloaded', '', 'pkg', 13, {}},
{'remove', 'pkg2'}
},
{
{'install_downloaded', '', 'pkg', 13},
{'install_downloaded', '', 'pkg', 13, {}},
{'remove', 'pkg2'}
}))
-- The order doesn't matter (since we are not sure if the planner is deterministic in that regard)
assert_true(equal(
{
{'install_downloaded', '', 'pkg', 13},
{'install_downloaded', '', 'pkg', 13, {}},
{'remove', 'pkg2'}
},
{
{'remove', 'pkg2'},
{'install_downloaded', '', 'pkg', 13}
{'install_downloaded', '', 'pkg', 13, {}}
}))
-- Package version changes the hash
assert_false(equal(
{
{'install_downloaded', '', 'pkg', 13},
{'install_downloaded', '', 'pkg', 13, {}},
{'remove', 'pkg2'}
},
{
{'install_downloaded', '', 'pkg', 14},
{'install_downloaded', '', 'pkg', 14, {}},
{'remove', 'pkg2'}
}))
-- Package name changes the hash
assert_false(equal(
{
{'install_downloaded', '', 'pkg', 13},
{'install_downloaded', '', 'pkg', 13, {}},
{'remove', 'pkg2'}
},
{
{'install_downloaded', '', 'pkg3', 13},
{'install_downloaded', '', 'pkg3', 13, {}},
{'remove', 'pkg2'}
}))
-- Package the operation changes the hash
assert_false(equal(
{
{'install_downloaded', '', 'pkg', 13},
{'install_downloaded', '', 'pkg', 13, {}},
{'remove', 'pkg2'}
},
{
......@@ -623,7 +629,7 @@ function test_approval_hash()
-- Omitting one of the tasks changes the hash
assert_false(equal(
{
{'install_downloaded', '', 'pkg', 13},
{'install_downloaded', '', 'pkg', 13, {}},
{'remove', 'pkg2'}
},
{
......@@ -634,7 +640,7 @@ end
function test_task_report()
assert_equal('', transaction.task_report())
assert_equal('', transaction.task_report('prefix '))
transaction.queue_install_downloaded('', "pkg1", 13)
transaction.queue_install_downloaded('', "pkg1", 13, {reboot = "finished"})
transaction.queue_remove("pkg2")
assert_equal([[
install 13 pkg1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment