diff --git a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h index 2dd3d0a..6a601dc 100644 --- a/ext_components/cp0_lvgl/include/cp0_lvgl_app.h +++ b/ext_components/cp0_lvgl/include/cp0_lvgl_app.h @@ -182,6 +182,7 @@ int cp0_sudo_run_argv_async_ex(const char *const *argv, /* Cancellation is idempotent while a request ID remains in the bounded recent * completion history. Returns -ENOENT only for an unknown or expired ID. */ int cp0_sudo_cancel(uint64_t request_id); +int cp0_sudo_queue_password(const char *password); int cp0_file_read_first_line(const char *path, char *out, int out_size); int cp0_desktop_exec_is_safe(const char *exec, char *reason, int reason_size); int cp0_network_default_info_read(cp0_eth_info_t *info); diff --git a/ext_components/cp0_lvgl/include/keyboard_input.h b/ext_components/cp0_lvgl/include/keyboard_input.h index 0f7f408..55bda15 100644 --- a/ext_components/cp0_lvgl/include/keyboard_input.h +++ b/ext_components/cp0_lvgl/include/keyboard_input.h @@ -41,6 +41,7 @@ extern volatile uint32_t LV_EVENT_KEYBOARD; void *keyboard_read_thread(void *argv); int cp0_keyboard_inject(uint32_t key_code, int key_state, uint32_t mods); +int cp0_keyboard_inject_text(const char *utf8); const char *kbd_state_name(int state); void kbd_dump_keymap_table(void); #ifdef __cplusplus diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c index cfb3b56..9496f8c 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_keyboard.c @@ -576,6 +576,32 @@ int cp0_keyboard_inject(uint32_t key_code, int key_state, uint32_t mods) return 0; } +int cp0_keyboard_inject_text(const char *utf8) +{ + if (!utf8) + return -1; + + const unsigned char *cursor = (const unsigned char *)utf8; + while (*cursor) { + size_t length = 1; + if ((*cursor & 0xe0) == 0xc0) length = 2; + else if ((*cursor & 0xf0) == 0xe0) length = 3; + else if ((*cursor & 0xf8) == 0xf0) length = 4; + + for (size_t i = 1; i < length; ++i) + if (!cursor[i] || (cursor[i] & 0xc0) != 0x80) + return -1; + + struct key_item item = {0}; + item.key_state = KBD_KEY_RELEASED; + memcpy(item.utf8, cursor, length); + snprintf(item.sym_name, sizeof(item.sym_name), "RPC_TEXT"); + enqueue_key(&item); + cursor += length; + } + return 0; +} + /* ============================================================ * Key repeat control * ============================================================ */ diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_rpc.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_rpc.cpp index 752db46..ff9ac73 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_rpc.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_rpc.cpp @@ -1,4 +1,5 @@ #include "cp0_lvgl.h" +#include "cp0_lvgl_app.h" #ifndef CP0_LVGL_USE_ZMQ_RPC #define CP0_LVGL_USE_ZMQ_RPC 0 @@ -47,6 +48,13 @@ const char *env_or_default(const char *name, const char *fallback) return value && value[0] ? value : fallback; } +void secure_clear(std::string &value) +{ + volatile char *data = value.empty() ? nullptr : &value[0]; + for (size_t i = 0; data && i < value.size(); ++i) data[i] = 0; + value.clear(); +} + bool capture_ppm(std::vector &out, std::string &error) { const char *device = env_or_default("APPLAUNCH_LINUX_FBDEV_DEVICE", "/dev/fb0"); @@ -186,6 +194,38 @@ void rpc_broker(std::shared_ptr context) send_text(rep, "OK key"); continue; } + if (command == "text") { + std::string value; + std::getline(input, value); + if (!value.empty() && value.front() == ' ') value.erase(0, 1); + if (value.empty()) { + send_text(rep, "ERR usage: text "); + continue; + } + if (cp0_keyboard_inject_text(value.c_str()) != 0) { + send_text(rep, "ERR invalid utf8 text"); + continue; + } + send_text(rep, "OK text"); + continue; + } + if (command == "password") { + std::string value; + std::getline(input, value); + if (!value.empty() && value.front() == ' ') value.erase(0, 1); + if (value.empty()) { + send_text(rep, "ERR usage: password "); + continue; + } + const int result = cp0_sudo_queue_password(value.c_str()); + secure_clear(value); + if (result != 0) { + send_text(rep, "ERR unable to queue password " + std::to_string(result)); + continue; + } + send_text(rep, "OK password"); + continue; + } if (command == "screenshot") { std::vector ppm; std::string error; diff --git a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp index b2a5b02..1977145 100644 --- a/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp +++ b/ext_components/cp0_lvgl/src/cp0/cp0_lvgl_settings.cpp @@ -34,6 +34,12 @@ struct ExtPortGpioMap { static constexpr const char *kHwIdPath = "/proc/cardputerzero_hw_id"; static constexpr ExtPortGpioMap kCardputerZeroGpioMap = {"/dev/gpiochip1", 3, 12, false, false}; static constexpr ExtPortGpioMap kFallbackGpioMap = {"/dev/gpiochip0", 17, 5, false, true}; +static constexpr const char *kGrove5vLedPaths[] = { + "/sys/class/leds/grove_5v_out/brightness", +}; +static constexpr const char *kExt5vLedPaths[] = { + "/sys/class/leds/ext_5v_out/brightness", +}; static int gpio_v2_get_value(int line_fd); @@ -217,22 +223,40 @@ class SettingsSystem int set_named_gpio(const char *name, int val) { std::lock_guard lock(gpio_mutex_); - const ExtPortGpioMap &map = active_gpio_map_locked(); - if (is_grove5v_name(name)) + if (is_grove5v_name(name)) { + int ret = write_led_value(kGrove5vLedPaths, val); + if (ret != -ENOENT) + return ret; + const ExtPortGpioMap &map = active_gpio_map_locked(); return set_gpio_value(map.chip_path, map.grove5v_line, map.grove5v_active_low, "GROVE5V", val); - if (is_ext5v_name(name)) + } + if (is_ext5v_name(name)) { + int ret = write_led_value(kExt5vLedPaths, val); + if (ret != -ENOENT) + return ret; + const ExtPortGpioMap &map = active_gpio_map_locked(); return set_gpio_value(map.chip_path, map.ext5v_line, map.ext5v_active_low, "EXT5V", val); + } return -EINVAL; } int get_named_gpio(const char *name) { std::lock_guard lock(gpio_mutex_); - const ExtPortGpioMap &map = active_gpio_map_locked(); - if (is_grove5v_name(name)) + if (is_grove5v_name(name)) { + int value = read_led_value(kGrove5vLedPaths); + if (value != -ENOENT) + return value; + const ExtPortGpioMap &map = active_gpio_map_locked(); return get_gpio_value(map.chip_path, map.grove5v_line, map.grove5v_active_low, "GROVE5V"); - if (is_ext5v_name(name)) + } + if (is_ext5v_name(name)) { + int value = read_led_value(kExt5vLedPaths); + if (value != -ENOENT) + return value; + const ExtPortGpioMap &map = active_gpio_map_locked(); return get_gpio_value(map.chip_path, map.ext5v_line, map.ext5v_active_low, "EXT5V"); + } return -EINVAL; } @@ -260,6 +284,48 @@ class SettingsSystem return value < 0 ? value : (value ^ (active_low ? 1 : 0)); } + template + static int write_led_value(const char *const (&paths)[N], int val) + { + const char value = val ? '1' : '0'; + for (const char *path : paths) { + int fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd < 0) { + if (errno == ENOENT) + continue; + return -errno; + } + ssize_t written = write(fd, &value, 1); + int err = written == 1 ? 0 : (written < 0 ? errno : EIO); + close(fd); + return -err; + } + return -ENOENT; + } + + template + static int read_led_value(const char *const (&paths)[N]) + { + for (const char *path : paths) { + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + if (errno == ENOENT) + continue; + return -errno; + } + char buf[16] = {}; + ssize_t size = read(fd, buf, sizeof(buf) - 1); + int err = size >= 0 ? 0 : errno; + close(fd); + if (err != 0) + return -err; + if (size == 0) + return -EIO; + return std::strtol(buf, nullptr, 10) != 0 ? 1 : 0; + } + return -ENOENT; + } + static int config_get_int(const char *key, int default_val) { int val = default_val; diff --git a/ext_components/cp0_lvgl/src/cp0_sudo_async.cpp b/ext_components/cp0_lvgl/src/cp0_sudo_async.cpp index 2d40a56..ca32c41 100644 --- a/ext_components/cp0_lvgl/src/cp0_sudo_async.cpp +++ b/ext_components/cp0_lvgl/src/cp0_sudo_async.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,9 @@ lv_group_t *g_prompt_group = nullptr; lv_timer_t *g_timer = nullptr; std::shared_ptr g_prompt_request; std::string g_password; +std::mutex g_queued_password_mutex; +std::string g_queued_password; +int64_t g_queued_password_deadline_ms = 0; int64_t now_ms() { @@ -142,6 +146,30 @@ bool authentication_error(const std::string &output) void key_event_cb(lv_event_t *event); void execute_actions(std::vector actions); +void submit_password(); +void apply_queued_password(void *); + +void clear_queued_password() +{ + std::lock_guard lock(g_queued_password_mutex); + secure_clear(g_queued_password); + g_queued_password_deadline_ms = 0; +} + +bool take_queued_password(std::string &password) +{ + std::lock_guard lock(g_queued_password_mutex); + if (g_queued_password.empty()) return false; + if (g_queued_password_deadline_ms > 0 && now_ms() > g_queued_password_deadline_ms) { + secure_clear(g_queued_password); + g_queued_password_deadline_ms = 0; + return false; + } + password = std::move(g_queued_password); + g_queued_password.clear(); + g_queued_password_deadline_ms = 0; + return true; +} void destroy_prompt() { @@ -158,6 +186,7 @@ void destroy_prompt() g_hint_label = nullptr; g_prompt_request.reset(); secure_clear(g_password); + clear_queued_password(); } void update_password_label() @@ -217,6 +246,7 @@ void create_prompt(const std::shared_ptr &request) lv_group_add_obj(g_prompt_group, g_overlay); lv_group_set_default(g_prompt_group); if (lv_indev_t *indev = lv_indev_get_next(nullptr)) lv_indev_set_group(indev, g_prompt_group); + lv_async_call(apply_queued_password, nullptr); } void show_auth_error(const std::shared_ptr &request) @@ -301,41 +331,31 @@ void run_worker(std::shared_ptr request, std::string password) if (!resolve_run_user(identity)) exit_code = -EPERM; else { std::string password_line = password + "\n"; - int auth_timeout = request->auth_timeout_ms; - if (auth_timeout > 0) - auth_timeout = static_cast(std::max(1, request->deadline_ms - now_ms())); - auto auth = cp0_runner::run({"sudo", "-k", "-S", "-p", "", "-v"}, - &password_line, {}, &request->cancel_requested, auth_timeout, - kMaxCapturedOutputBytes, identity.uid, identity.gid, identity.name, - identity.home, identity.shell); - secure_clear(password_line); - if (auth.exit_code != 0) { - exit_code = auth.exit_code; - result = auth.exit_code == -ECANCELED ? CP0_SUDO_RESULT_CANCELLED : - auth.exit_code == -ETIMEDOUT ? CP0_SUDO_RESULT_TIMED_OUT : - authentication_error(auth.output) ? CP0_SUDO_RESULT_AUTH_FAILED : - CP0_SUDO_RESULT_EXEC_FAILED; - auto actions = g_coordinator.worker_auth_result(request->id, result, exit_code, now_ms()); - secure_clear(password); - if (!actions.empty() && !post_actions(actions)) - g_coordinator.requeue_actions(std::move(actions)); - return; - } std::vector command; if (request->use_login_shell) - command = {"sudo", "-n", "--", identity.shell, "-c", request->argv.front()}; + command = {"sudo", "-k", "-S", "-p", "", "--", identity.shell, "-c", + request->argv.front()}; else { - command = {"sudo", "-n", "--"}; + command = {"sudo", "-k", "-S", "-p", "", "--"}; command.insert(command.end(), request->argv.begin(), request->argv.end()); } - auto execution = cp0_runner::run(std::move(command), nullptr, + auto execution = cp0_runner::run(std::move(command), &password_line, [request](const char *data, size_t size) { stream_output(request, data, size); }, &request->cancel_requested, request->exec_timeout_ms, kMaxCapturedOutputBytes, identity.uid, identity.gid, identity.name, identity.home, identity.shell); + secure_clear(password_line); exit_code = execution.exit_code; result = exit_code == -ECANCELED ? CP0_SUDO_RESULT_CANCELLED : exit_code == -ETIMEDOUT ? CP0_SUDO_RESULT_TIMED_OUT : + authentication_error(execution.output) ? CP0_SUDO_RESULT_AUTH_FAILED : exit_code == 0 ? CP0_SUDO_RESULT_SUCCESS : CP0_SUDO_RESULT_EXEC_FAILED; + if (result == CP0_SUDO_RESULT_AUTH_FAILED) { + auto actions = g_coordinator.worker_auth_result(request->id, result, exit_code, now_ms()); + secure_clear(password); + if (!actions.empty() && !post_actions(actions)) + g_coordinator.requeue_actions(std::move(actions)); + return; + } } #endif secure_clear(password); @@ -398,6 +418,18 @@ void submit_password() execute_actions(g_coordinator.submit_password(g_prompt_request->id)); } +void apply_queued_password(void *) +{ + if (!g_prompt_request || g_coordinator.state(g_prompt_request->id) != cp0_sudo::State::PROMPT) + return; + std::string password; + if (!take_queued_password(password)) return; + secure_clear(g_password); + g_password = std::move(password); + update_password_label(); + submit_password(); +} + void key_event_cb(lv_event_t *event) { auto *key = static_cast(lv_event_get_param(event)); @@ -516,6 +548,24 @@ extern "C" int cp0_sudo_cancel(uint64_t request_id) return 0; } +extern "C" int cp0_sudo_queue_password(const char *password) +{ + if (!password || !password[0]) return -EINVAL; + const size_t length = std::strlen(password); + if (length > kMaxPasswordBytes) return -E2BIG; + { + std::lock_guard lock(g_queued_password_mutex); + secure_clear(g_queued_password); + g_queued_password.assign(password, length); + g_queued_password_deadline_ms = now_ms() + 60000; + } + if (lv_async_call(apply_queued_password, nullptr) != LV_RESULT_OK) { + clear_queued_password(); + return -EIO; + } + return 0; +} + extern "C" void init_sudo_signals(void) { cp0_signal_sudo_argv_async.append([](std::list args, int auth_timeout_ms, diff --git a/projects/APPLaunch/main/src/main.cpp b/projects/APPLaunch/main/src/main.cpp index 5100b50..b3a6b13 100644 --- a/projects/APPLaunch/main/src/main.cpp +++ b/projects/APPLaunch/main/src/main.cpp @@ -1,3 +1,7 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + /* * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD * @@ -7,6 +11,7 @@ #include "lvgl/lvgl.h" #include "lvgl/demos/lv_demos.h" #include +#include #include #include #include @@ -26,6 +31,28 @@ static sem_t lvgl_sem; +static void lvgl_wait(uint32_t milliseconds) +{ + struct timespec deadline; +#if defined(__linux__) + clock_gettime(CLOCK_MONOTONIC, &deadline); +#else + clock_gettime(CLOCK_REALTIME, &deadline); +#endif + deadline.tv_nsec += (milliseconds % 1000) * 1000000; + deadline.tv_sec += milliseconds / 1000 + deadline.tv_nsec / 1000000000; + deadline.tv_nsec %= 1000000000; + + int result; + do { +#if defined(__linux__) + result = sem_clockwait(&lvgl_sem, CLOCK_MONOTONIC, &deadline); +#else + result = sem_timedwait(&lvgl_sem, &deadline); +#endif + } while (result != 0 && errno == EINTR); +} + static void lvgl_resume_cb(void *data) { sem_post(&lvgl_sem); } @@ -60,12 +87,7 @@ int main(void) if (ms == LV_NO_TIMER_READY) { sem_wait(&lvgl_sem); // 无定时器,阻塞等事件 } else { - struct timespec ts; - clock_gettime(CLOCK_REALTIME, &ts); - ts.tv_nsec += (ms % 1000) * 1000000; - ts.tv_sec += ms / 1000 + ts.tv_nsec / 1000000000; - ts.tv_nsec %= 1000000000; - sem_timedwait(&lvgl_sem, &ts); // 定时唤醒 或 被事件提前唤醒 + lvgl_wait(ms); // 定时唤醒或被事件提前唤醒,不受系统时间调整影响 } } diff --git a/scripts/cp0_rpc.py b/scripts/cp0_rpc.py index 4fbbd46..2d8f8cf 100644 --- a/scripts/cp0_rpc.py +++ b/scripts/cp0_rpc.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +import getpass import pathlib import sys import time @@ -52,6 +53,10 @@ def main() -> int: tap = sub.add_parser("tap") tap.add_argument("key", type=key_code) tap.add_argument("--duration", type=float, default=0.08) + text = sub.add_parser("text") + text.add_argument("value") + password = sub.add_parser("password") + password.add_argument("value", nargs="?") hold = sub.add_parser("hold") hold.add_argument("key", type=key_code) hold.add_argument("seconds", type=float) @@ -80,6 +85,11 @@ def main() -> int: send_key(socket, args.key, 1) time.sleep(max(0.0, args.duration)) send_key(socket, args.key, 0) + elif args.command == "text": + request(socket, f"text {args.value}") + elif args.command == "password": + value = args.value if args.value is not None else getpass.getpass("Password: ") + request(socket, f"password {value}") elif args.command == "hold": send_key(socket, args.key, 1) try: