~aleteoryx/muditaos

752147e8145102ce0a64703757d927288870489d — Lefucjusz 2 years ago 76a62e8
[BH-1881] Add progress bar to update process

* Modified Lua update scripts to show simple
progress bar during update process in
Recovery.
* Fixed regression with failing factory
reset on Pure.
* Split Lua scripts per product.
M harmony_changelog.md => harmony_changelog.md +1 -0
@@ 9,6 9,7 @@
* Added new 32px and 170px fonts
* Added new clock face with quotes
* Added a backend for quotes on home screen
* Added progress bar to update process

### Changed / Improved
* Updated FSL drivers from NXP

M module-services/service-desktop/include/service-desktop/Constants.hpp => module-services/service-desktop/include/service-desktop/Constants.hpp +1 -2
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 11,5 11,4 @@ namespace sdesktop::paths
    inline constexpr auto syncFilename           = "sync.tar";
    inline constexpr auto backupFilename         = "backup.tar";
    inline constexpr auto recoveryStatusFilename = "recovery_status.json";

} // namespace sdesktop::paths

M products/BellHybrid/BinaryAssetsVersions.cmake => products/BellHybrid/BinaryAssetsVersions.cmake +1 -1
@@ 1,6 1,6 @@
# This file sets versions of downloaded binaries for release packaging purposes

if( NOT DEFINED ECOBOOT_BIN_VERSION)
if (NOT DEFINED ECOBOOT_BIN_VERSION)
    set(ECOBOOT_BIN_VERSION 2.0.8 CACHE STRING "bootloader binary version to download from bootloader release page")
endif()


M scripts/lua/install.sh => scripts/lua/install.sh +3 -3
@@ 1,5 1,5 @@
#!/usr/bin/env bash
# Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
# Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
# For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

function validate_product_selection() {


@@ 28,10 28,10 @@ if validate_product_selection; then
    cd "$parent_path"

    mkdir -p ${DESTINATION}
    cp products/${PRODUCT}/scripts/*.lua ${DESTINATION}
    cp -r products/${PRODUCT}/assets ${DESTINATION}/assets/
    cp -r share ${DESTINATION}/share
    cp -r migration/migration.lua ${DESTINATION}/share
    cp *.lua ${DESTINATION}
    rm ${DESTINATION}/update_udm.lua
    cd -
fi
\ No newline at end of file
fi

A scripts/lua/products/BellHybrid/scripts/backup.lua => scripts/lua/products/BellHybrid/scripts/backup.lua +51 -0
@@ 0,0 1,51 @@
local helpers = require('helpers')
local recovery = require('recovery')
local paths = require('paths')
local tar = require('ltar')
local lfs = require('lfs')
local backup = {}

-- Match only files with '.db' extensions and omit such files inside subdirectories
local match_db_files = '^[^%/]*%.db$'

backup.script_name = "backup.lua"
backup.img_in_progress = "assets/gui_image_backup_in_progress.bin"
backup.img_success = "assets/gui_image_backup_success.bin"
backup.img_failure = "assets/gui_image_backup_failed.bin"
backup.img_in_progress_wait = true

local function check_available_space()
    local db_size = helpers.dir_size_filtered(paths.db_dir, match_db_files)
    local version_size = lfs.attributes(paths.version_file, 'size')
    local available_space = recovery.sys.free_space(recovery.sys.user())
    -- Multiply the result by two due to the internal padding inside tar
    local required_space = (db_size + version_size) * 2

    print(string.format("Checking disk space:\nNeeded space: %d bytes, available space: %d bytes", required_space,
        available_space))

    assert(available_space > required_space, "Not enough free space on user disk")
end

local function create_temp_dir()
    if not helpers.exists(paths.temp_dir) then
        print(string.format("'%s' does not exist, creating", paths.temp_dir))
        lfs.mkdir(paths.temp_dir)
    end
end

local function create_backup_package()
    print(string.format("Creating backup package from the contents of '%s' directory and saving it into '%s'",
        paths.db_dir, paths.backup_file))
    tar.create_from_path_regex(paths.db_dir, paths.backup_file, match_db_files)
    tar.append_file(paths.version_file, paths.backup_file)
end

function backup.execute()
    check_available_space()
    create_temp_dir()
    create_backup_package()
end

return backup


A scripts/lua/products/BellHybrid/scripts/entry.lua => scripts/lua/products/BellHybrid/scripts/entry.lua +88 -0
@@ 0,0 1,88 @@
local helpers = require('helpers')
local paths = require('paths')
local rec = require('recovery')
local backup = require('backup')
local restore = require('restore')
local update = require('update')
local factory = require('factory')

local scripts = {}
scripts[rec.sys.boot_reason_codes.backup] = backup
scripts[rec.sys.boot_reason_codes.restore] = restore
scripts[rec.sys.boot_reason_codes.update] = update
scripts[rec.sys.boot_reason_codes.factory] = factory

--- Display image
-- @function display_image
-- @param path path to raw image data
-- @param should_wait whether to wait after showing the image
local function display_image(path, should_wait)
    local raw_data = helpers.read_whole_file(path)
    rec.gui.clear()
    rec.gui.display_raw_img(600, 480, raw_data)
    -- Give some time to user to view the displayed image
    if should_wait then
        rec.sys.sleep(5)
    end
end

local function print_recovery_info()
    print(string.format("PureRecovery version: %s, branch: %s, revision: %s", rec.version(), rec.branch(),
        rec.revision()))
end

local function print_boot_reason()
    print(string.format("Boot reason: %s", rec.sys.boot_reason_str()))
end

local function generate_report_file(boot_reason_str, success, message)
    local file_path = paths.temp_dir .. "/recovery_status.json"
    local body = string.format(
        "{\"version\": \"%s\",\"branch\": \"%s\",\"revision\": \"%s\",\"operation\": \"%s\",\"successful\": %s,\"message\": \"%s\"}",
        rec.version(), rec.branch(), rec.revision(), boot_reason_str, tostring(success), message)
    local fd = assert(io.open(file_path, 'w'))
    fd:write(body)
    fd:close()
end

local function handle_boot_reason()
    local boot_reason = rec.sys.boot_reason()
    local boot_reason_str = rec.sys.boot_reason_str()
    rec.sys.set_boot_reason(rec.sys.boot_reason_codes.os)
    return boot_reason, boot_reason_str
end

local function handle_script_success(boot_reason)
    display_image(scripts[boot_reason].img_success, true)
    print("Finished successfully")
end

local function handle_script_failure(boot_reason, message)
    display_image(scripts[boot_reason].img_failure, true)
    print(message)
end

local function handle_exit(boot_reason_str, status, message)
    generate_report_file(boot_reason_str, status, message)
    rec.gui.clear()
end

local function invoke_script(boot_reason)
    print(string.format("Executing '%s' script...", scripts[boot_reason].script_name))
    display_image(scripts[boot_reason].img_in_progress, scripts[boot_reason].img_in_progress_wait)

    local status, message = pcall(scripts[boot_reason].execute)
    if status then
        handle_script_success(boot_reason)
        return true, "ok"
    else
        handle_script_failure(boot_reason, message)
        return false, message
    end
end

print_recovery_info()
print_boot_reason()
local boot_reason, boot_reason_str = handle_boot_reason()
local status, message = invoke_script(boot_reason)
handle_exit(boot_reason_str, status, message)

R scripts/lua/factory.lua => scripts/lua/products/BellHybrid/scripts/factory.lua +1 -0
@@ 7,6 7,7 @@ factory.script_name = "factory.lua"
factory.img_in_progress = "assets/gui_image_factory_reset_in_progress.bin"
factory.img_success = "assets/gui_image_factory_reset_success.bin"
factory.img_failure = "assets/gui_image_factory_reset_failed.bin"
factory.img_in_progress_wait = true

local function remove_old_databases()
    print(string.format("Removing old databases from '%s'", paths.db_dir))

A scripts/lua/products/BellHybrid/scripts/restore.lua => scripts/lua/products/BellHybrid/scripts/restore.lua +115 -0
@@ 0,0 1,115 @@
local helpers = require('helpers')
local recovery = require('recovery')
local paths = require('paths')
local consts = require('consts')
local ltar = require('ltar')
local lfs = require('lfs')
local json = require('lunajson')
local migration = require('migration')
local restore = {}

local unpacked_backup_dir = paths.temp_dir .. "/backup"

restore.script_name = "restore.lua"
restore.img_in_progress = "assets/gui_image_restore_in_progress.bin"
restore.img_success = "assets/gui_image_restore_success.bin"
restore.img_failure = "assets/gui_image_restore_failed.bin"
restore.img_in_progress_wait = true

local function check_available_space()
    local backup_size = lfs.attributes(paths.backup_file, 'size')
    local available_space = recovery.sys.free_space(recovery.sys.user())

    print(string.format("Checking disk space:\nNeeded space: %d bytes, available space: %d bytes", backup_size,
        available_space))

    assert(available_space > backup_size, "Not enough free space on user disk")
end

local function unpack_backup()
    if helpers.exists(unpacked_backup_dir) then
        print(string.format("Directory '%s' exists, removing", unpacked_backup_dir))
        helpers.rmdir(unpacked_backup_dir)
    end
    lfs.mkdir(unpacked_backup_dir)
    print(string.format("Unpacking backup file '%s' to '%s'", paths.backup_file, unpacked_backup_dir))
    ltar.unpack(paths.backup_file, unpacked_backup_dir)
end

local function get_db_array_from_file(file)
    local contents = helpers.read_whole_file(file)
    local root = json.decode(contents)
    local set = {}
    for _, entry in pairs(root.databases) do
        set[entry.name] = tonumber(entry.version)
    end
    return set
end

local function get_db_array_from_path(path)
    local set = {}
    for file in lfs.dir(path) do
        local file_path = path .. "/" .. file
        if file ~= "." and file ~= ".." then
            if lfs.attributes(file_path, "mode") == "file" then
                set[helpers.strip_from_extension(file)] = true;
            end
        end
    end
    return set
end

local function build_db_set()
    local system_db_set = get_db_array_from_file(paths.version_file)
    local backup_db_set = get_db_array_from_path(unpacked_backup_dir)
    local set = {}
    for name, version in pairs(system_db_set) do
        if backup_db_set[name] then
            set[name] = tonumber(version)
        end
    end
    return set
end

local function perform_db_migration()
    print("Performing database migration")

    local result = migration.migrate(unpacked_backup_dir, paths.migration_scripts_dir, build_db_set())
    assert(result == migration.retcode.OK, string.format("Database migration process failed with %d", result))
end

local function sync_databases()
    print("Syncing databases:")

    for name, _ in pairs(build_db_set()) do
        local destination = paths.db_dir .. "/" .. name .. ".db"
        local source = unpacked_backup_dir .. "/" .. name .. ".db"
        print(string.format("Replacing '%s' with '%s'", destination, source));
        assert(os.remove(destination))
        helpers.copy_file(source, destination)
    end
end

local function remove_cache()
    if helpers.exists(paths.file_indexer_cache) then
        print(string.format("Removing cache file '%s'", paths.file_indexer_cache))
        assert(os.remove(paths.file_indexer_cache))
    end
end

local function cleanup()
    if helpers.exists(unpacked_backup_dir) then
        helpers.rmdir(unpacked_backup_dir)
    end
end

function restore.execute()
    check_available_space()
    unpack_backup()
    perform_db_migration()
    sync_databases()
    remove_cache()
    cleanup()
end

return restore

A scripts/lua/products/BellHybrid/scripts/update.lua => scripts/lua/products/BellHybrid/scripts/update.lua +145 -0
@@ 0,0 1,145 @@
local update = {}
local lfs = require('lfs')
local json = require('lunajson')
local paths = require('paths')
local consts = require('consts')
local helpers = require('helpers')
local recovery = require('recovery')
local migration = require('migration')

update.script_name = "update.lua"
update.img_in_progress = "assets/gui_image_update_in_progress.bin"
update.img_success = "assets/gui_image_update_success.bin"
update.img_failure = "assets/gui_image_update_failed.bin"
update.img_in_progress_wait = false -- To display progress bar immediately after the script has started

-- Match only files with '.db' extensions and omit such files inside subdirectories
local match_db_files = '^[^%/]*%.db$'

-- Percent values to be shown on progress bar taking each step's processing time into account
local update_stages = {
    init = 0,
    stage1 = 4,
    stage2 = 8,
    stage3 = 72,
    stage4 = 75,
    stage5 = 77,
    stage6 = 80,
    stage7 = 82,
    stage8 = 85,
    done = 100
}

local function update_progress(percent)
    local x, y = 90, 354
    local height_empty, height_full = 4, 8
    local width = 420
    recovery.gui.draw_progress_bar(x, y, width, height_empty, height_full, percent)
    recovery.gui.refresh_fast()
end

local function build_db_set(file)
    local contents = helpers.read_whole_file(file)
    local root = json.decode(contents)
    local set = {}
    for _, entry in pairs(root.databases) do
        set[entry.name] = tonumber(entry.version)
    end
    return set
end

local function purge_target_slot()
    local target_dir = recovery.sys.target_slot()
    print(string.format("Removing target slot content, '%s'", target_dir))
    helpers.rmdir_content(target_dir)
end

local function copy_update_package()
    local target_dir = recovery.sys.target_slot()
    print(string.format("Copying content of the update package '%s to '%s'", paths.update_dir, target_dir))
    helpers.copy_dir(paths.update_dir, target_dir)
end

local function copy_databases()
    local from = paths.db_dir
    local to = recovery.sys.target_slot() .. "/db"
    print(string.format("Copying databases from '%s' to '%s'", from, to))
    helpers.copy_dir_filtered(from, to, match_db_files)
end

local function create_directories()
    print("Creating 'log' and 'crash_dumps' directories")

    local target_dir = recovery.sys.target_slot()
    lfs.mkdir(target_dir .. "/log")
    lfs.mkdir(target_dir .. "/crash_dumps")
    lfs.mkdir(target_dir .. "/var")
end

local function migrate_db()
    local version_file = paths.update_dir .. "/" .. consts.version_file

    print("Performing database migration")
    local dbset = build_db_set(version_file)
    local result = migration.migrate(paths.target.db_dir, paths.target.migration_scripts_dir, dbset)
    assert(result == migration.retcode.OK, string.format("Database migration process failed with %d", result))
end

local function remove_cache()
    if helpers.exists(paths.target.file_indexer_cache) then
        print(string.format("Removing cache file '%s'", paths.target.file_indexer_cache))
        assert(os.remove(paths.target.file_indexer_cache))
    end
end

local function copy_var_directory()
    local from = paths.var_dir
    local to = paths.target.var_dir
    print(string.format("Copying '%s' to '%s'", from, to))
    helpers.copy_dir(from, to)
end

local function enter()
    -- Mark the current slot as successful 
    recovery.bootctrl.mark_as_successful()
    -- Mark the target slot as unbootable
    recovery.bootctrl.mark_as_unbootable(recovery.bootctrl.get_next_active())
end

local function exit()
    print("Finishing update process...")

    helpers.rmdir(paths.update_dir)
    os.remove(paths.update_file)

    -- Update the working directory to the newly updated scripts directory
    lfs.chdir(recovery.sys.target_slot() .. "/scripts")

    recovery.bootctrl.mark_as_bootable(recovery.bootctrl.get_next_active())
    recovery.bootctrl.mark_as_active(recovery.bootctrl.get_next_active())
    recovery.sys.set_os_boot_status(false)
end

function update.execute()
    update_progress(update_stages.init)
    enter()
    update_progress(update_stages.stage1)
    purge_target_slot()
    update_progress(update_stages.stage2)
    copy_update_package()
    update_progress(update_stages.stage3)
    copy_databases()
    update_progress(update_stages.stage4)
    create_directories()
    update_progress(update_stages.stage5)
    migrate_db()
    update_progress(update_stages.stage6)
    copy_var_directory()
    update_progress(update_stages.stage7)
    remove_cache()
    update_progress(update_stages.stage8)
    exit()
    update_progress(update_stages.done)
end

return update

R scripts/lua/backup.lua => scripts/lua/products/PurePhone/scripts/backup.lua +0 -0
R scripts/lua/entry.lua => scripts/lua/products/PurePhone/scripts/entry.lua +0 -1
@@ 83,4 83,3 @@ print_boot_reason()
local boot_reason, boot_reason_str = handle_boot_reason()
local status, message = invoke_script(boot_reason)
handle_exit(boot_reason_str, status, message)


A scripts/lua/products/PurePhone/scripts/factory.lua => scripts/lua/products/PurePhone/scripts/factory.lua +34 -0
@@ 0,0 1,34 @@
local helpers = require('helpers')
local paths = require('paths')

local factory = {}

factory.script_name = "factory.lua"
factory.img_in_progress = "assets/gui_image_factory_reset_in_progress.bin"
factory.img_success = "assets/gui_image_factory_reset_success.bin"
factory.img_failure = "assets/gui_image_factory_reset_failed.bin"

local function remove_old_databases()
    print(string.format("Removing old databases from '%s'", paths.db_dir))
    helpers.rm_files_from_dir(paths.db_dir)
end

local function copy_clean_databases()
    print(string.format("Copying clean databases from '%s' to '%s'", paths.db_factory_dir, paths.db_dir))
    helpers.copy_dir(paths.db_factory_dir, paths.db_dir)
end

local function remove_cache()
    if helpers.exists(paths.file_indexer_cache) then
        print(string.format("Removing cache file '%s'", paths.file_indexer_cache))
        assert(os.remove(paths.file_indexer_cache))
    end
end

function factory.execute()
    remove_old_databases()
    copy_clean_databases()
    remove_cache()
end

return factory

R scripts/lua/restore.lua => scripts/lua/products/PurePhone/scripts/restore.lua +0 -0
R scripts/lua/update.lua => scripts/lua/products/PurePhone/scripts/update.lua +1 -1
@@ 78,7 78,7 @@ local function copy_var_directory()
end

local function enter()
    -- Mark the current slot as successful 
    -- Mark the current slot as successful
    recovery.bootctrl.mark_as_successful()
    -- Mark the target slot as unbootable
    recovery.bootctrl.mark_as_unbootable(recovery.bootctrl.get_next_active())

M scripts/lua/share/paths.lua => scripts/lua/share/paths.lua +1 -1
@@ 14,7 14,7 @@ paths.db_factory_dir = paths.db_dir .. "/factory"
paths.temp_dir = user_dir .. "/temp"
paths.update_dir = user_dir .. "/temp/update"
paths.migration_scripts_dir = paths.db_dir .. "/migration"
paths.user_relaxation_file = user_dir .. "/media/app/relaxation"
paths.user_relaxation_file = user_dir .. "/media/app/relaxation" -- Only for Harmony

local target = {}
target.var_dir = target_dir .. "/var"