Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ projects/lcd_test
.sconsign.dblite
zeromp3
*.pyc
docs/rtc/applaunch_rtc_flow.html
2 changes: 2 additions & 0 deletions ext_components/cp0_lvgl/src/cp0/cp0_lvgl.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ void cp0_lvgl_init(void)
init_config();
init_pty();
init_freambuffer_disp();
if (lv_display_get_default() == NULL)
return;
init_input();
init_rpc();
init_audio();
Expand Down
36 changes: 36 additions & 0 deletions ext_components/cp0_lvgl/src/cp0/cp0_lvgl_freambuffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ static int find_st7789v_fbdev(char *dev_path, size_t buf_size)

void init_freambuffer_disp()
{
#if LV_USE_LINUX_DRM
const char *device = getenv("LV_LINUX_DRM_DEVICE");
char *detected_device = NULL;
if (device == NULL || device[0] == '\0') {
detected_device = lv_linux_drm_find_device_path();
device = detected_device;
}

if (device == NULL || device[0] == '\0') {
fprintf(stderr, "Failed to find a connected DRM device\n");
lv_free(detected_device);
return;
}

printf("Using DRM device: %s\n", device);
lv_display_t *disp = lv_linux_drm_create();
if (disp == NULL) {
fprintf(stderr, "Failed to create DRM display\n");
lv_free(detected_device);
return;
}

if (lv_linux_drm_set_file(disp, device, -1) != LV_RESULT_OK) {
fprintf(stderr, "Failed to initialize DRM device: %s\n", device);
lv_display_delete(disp);
lv_free(detected_device);
return;
}

lv_free(detected_device);

printf("DRM resolution: %dx%d\n",
(int)lv_display_get_horizontal_resolution(disp),
(int)lv_display_get_vertical_resolution(disp));
#else
lv_display_t *disp = lv_linux_fbdev_create();
if (disp == NULL) {
printf("Failed to create fbdev display!\n");
Expand All @@ -63,4 +98,5 @@ void init_freambuffer_disp()
lv_coord_t w = lv_display_get_horizontal_resolution(disp);
lv_coord_t h = lv_display_get_vertical_resolution(disp);
printf("Framebuffer resolution: %dx%d\n", w, h);
#endif
}
8 changes: 5 additions & 3 deletions ext_components/cp0_lvgl/src/cp0/cp0_lvgl_process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class ProcessSystem
(void)keep_root;
return -1;
#else
cp0_process_group::enable_subreaper();
pid_t pid = fork();
if (pid < 0)
return -1;
Expand All @@ -149,9 +150,10 @@ class ProcessSystem
#if !defined(_WIN32)
if (pid <= 0)
return;
killpg(static_cast<pid_t>(pid), SIGTERM);
int status = 0;
waitpid(static_cast<pid_t>(pid), &status, WNOHANG);
if (!cp0_process_group::terminate_and_reap(static_cast<pid_t>(pid),
static_cast<pid_t>(pid)))
std::fprintf(stderr, "[process] failed to stop and reap pgid=%d\n",
static_cast<int>(pid));
#else
(void)pid;
#endif
Expand Down
71 changes: 71 additions & 0 deletions ext_components/cp0_lvgl/src/cp0_external_process_group.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
#if !defined(_WIN32)

#include <cerrno>
#include <chrono>
#include <csignal>
#include <thread>
#include <sys/types.h>
#include <sys/wait.h>
#if defined(__linux__)
Expand Down Expand Up @@ -47,6 +49,75 @@ inline void reap_available(pid_t pgid, pid_t leader, int &leader_status,
}
}

inline bool exited_unreaped(pid_t pid)
{
if (pid <= 0) return false;
siginfo_t info{};
if (waitid(P_PID, pid, &info, WEXITED | WNOHANG | WNOWAIT) < 0)
return errno == ECHILD;
return info.si_pid == pid;
}

inline bool defer_reap(pid_t pgid, pid_t leader,
std::chrono::milliseconds poll_interval =
std::chrono::milliseconds(100))
{
try {
std::thread([pgid, leader, poll_interval]() {
int leader_status = 0;
bool leader_reaped = false;
while (exists(pgid)) {
reap_available(pgid, leader, leader_status, leader_reaped);
if (exists(pgid)) std::this_thread::sleep_for(poll_interval);
}
reap_available(pgid, leader, leader_status, leader_reaped);
}).detach();
} catch (...) {
return false;
}
return true;
}

inline bool terminate_and_reap(pid_t pgid, pid_t leader,
std::chrono::milliseconds grace = std::chrono::seconds(2),
std::chrono::milliseconds poll_interval =
std::chrono::milliseconds(10),
std::chrono::milliseconds kill_wait =
std::chrono::seconds(2))
{
if (pgid <= 0) return false;

int leader_status = 0;
bool leader_reaped = false;
if (kill(-pgid, SIGTERM) < 0) {
if (errno != ESRCH) return false;
reap_available(pgid, leader, leader_status, leader_reaped);
return true;
}

// Keep the leader as a zombie during the grace period. Reaping it could
// allow its PID/PGID to be reused before the forced group kill.
const auto term_deadline = std::chrono::steady_clock::now() + grace;
while (exists(pgid) && !exited_unreaped(leader) &&
std::chrono::steady_clock::now() < term_deadline)
std::this_thread::sleep_for(poll_interval);

if (exists(pgid) && kill(-pgid, SIGKILL) < 0 && errno != ESRCH)
return false;

const auto kill_deadline = std::chrono::steady_clock::now() + kill_wait;
while (exists(pgid) && std::chrono::steady_clock::now() < kill_deadline) {
reap_available(pgid, leader, leader_status, leader_reaped);
if (exists(pgid)) std::this_thread::sleep_for(poll_interval);
}
reap_available(pgid, leader, leader_status, leader_reaped);
if (!exists(pgid)) return leader_reaped;

// A task in uninterruptible sleep cannot act on SIGKILL yet. Do not block
// the caller indefinitely; retain ownership of this PGID until it exits.
return defer_reap(pgid, leader);
}

} // namespace cp0_process_group

#endif
8 changes: 5 additions & 3 deletions ext_components/cp0_lvgl/src/sdl/sdl_lvgl_process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class ProcessSystem
(void)keep_root;
return -1;
#else
cp0_process_group::enable_subreaper();
pid_t pid = fork();
if (pid < 0)
return -1;
Expand All @@ -147,9 +148,10 @@ class ProcessSystem
#if !defined(_WIN32)
if (pid <= 0)
return;
killpg(static_cast<pid_t>(pid), SIGTERM);
int status = 0;
waitpid(static_cast<pid_t>(pid), &status, WNOHANG);
if (!cp0_process_group::terminate_and_reap(static_cast<pid_t>(pid),
static_cast<pid_t>(pid)))
std::fprintf(stderr, "[process] failed to stop and reap pgid=%d\n",
static_cast<int>(pid));
#else
(void)pid;
#endif
Expand Down
119 changes: 119 additions & 0 deletions ext_components/cp0_lvgl/tests/test_external_process_group.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
#include <cassert>
#include <chrono>
#include <csignal>
#include <cerrno>
#include <thread>
#include <sys/wait.h>
#include <unistd.h>

int main()
Expand Down Expand Up @@ -43,4 +45,121 @@ int main()
assert(leader_reaped);
assert(WIFEXITED(leader_status) && WEXITSTATUS(leader_status) == 0);
assert(observed_leader_exit_with_live_group);
assert(!cp0_process_group::terminate_and_reap(0, 0));

int graceful_ready[2];
assert(pipe(graceful_ready) == 0);
leader = fork();
assert(leader >= 0);
if (leader == 0) {
close(graceful_ready[0]);
setpgid(0, 0);
pid_t worker = fork();
if (worker < 0) _exit(2);
if (worker == 0) {
const char value = 'x';
if (write(graceful_ready[1], &value, 1) != 1) _exit(3);
for (;;) pause();
}
for (;;) pause();
}

close(graceful_ready[1]);
setpgid(leader, leader);
char graceful_value = 0;
assert(read(graceful_ready[0], &graceful_value, 1) == 1 && graceful_value == 'x');
close(graceful_ready[0]);
assert(cp0_process_group::terminate_and_reap(
leader, leader, milliseconds(250), milliseconds(5)));
errno = 0;
assert(waitpid(-leader, nullptr, WNOHANG) == -1 && errno == ECHILD);
assert(!cp0_process_group::exists(leader));

int mixed_ready[2];
assert(pipe(mixed_ready) == 0);
leader = fork();
assert(leader >= 0);
if (leader == 0) {
close(mixed_ready[0]);
setpgid(0, 0);
pid_t worker = fork();
if (worker < 0) _exit(2);
if (worker == 0) {
signal(SIGTERM, SIG_IGN);
const char value = 'x';
if (write(mixed_ready[1], &value, 1) != 1) _exit(3);
for (;;) pause();
}
for (;;) pause();
}

close(mixed_ready[1]);
setpgid(leader, leader);
char mixed_value = 0;
assert(read(mixed_ready[0], &mixed_value, 1) == 1 && mixed_value == 'x');
close(mixed_ready[0]);
assert(cp0_process_group::terminate_and_reap(
leader, leader, milliseconds(50), milliseconds(5)));
errno = 0;
assert(waitpid(-leader, nullptr, WNOHANG) == -1 && errno == ECHILD);
assert(!cp0_process_group::exists(leader));

int ready[2];
assert(pipe(ready) == 0);
leader = fork();
assert(leader >= 0);
if (leader == 0) {
close(ready[0]);
setpgid(0, 0);
signal(SIGTERM, SIG_IGN);
pid_t worker = fork();
if (worker < 0) _exit(2);
if (worker == 0) {
signal(SIGTERM, SIG_IGN);
const char value = 'x';
if (write(ready[1], &value, 1) != 1) _exit(3);
for (;;) pause();
}
for (;;) pause();
}

close(ready[1]);
setpgid(leader, leader);
char value = 0;
assert(read(ready[0], &value, 1) == 1 && value == 'x');
close(ready[0]);
assert(cp0_process_group::terminate_and_reap(
leader, leader, milliseconds(50), milliseconds(5)));
errno = 0;
assert(waitpid(-leader, nullptr, WNOHANG) == -1 && errno == ECHILD);
assert(!cp0_process_group::exists(leader));

int deferred_ready[2];
assert(pipe(deferred_ready) == 0);
leader = fork();
assert(leader >= 0);
if (leader == 0) {
close(deferred_ready[0]);
setpgid(0, 0);
signal(SIGTERM, SIG_IGN);
const char value = 'x';
if (write(deferred_ready[1], &value, 1) != 1) _exit(3);
for (;;) pause();
}

close(deferred_ready[1]);
setpgid(leader, leader);
char deferred_value = 0;
assert(read(deferred_ready[0], &deferred_value, 1) == 1 && deferred_value == 'x');
close(deferred_ready[0]);
assert(cp0_process_group::defer_reap(leader, leader, milliseconds(5)));
std::this_thread::sleep_for(milliseconds(20));
assert(cp0_process_group::exists(leader));
assert(kill(-leader, SIGKILL) == 0);
const auto deferred_deadline = steady_clock::now() + seconds(1);
while (cp0_process_group::exists(leader) && steady_clock::now() < deferred_deadline)
std::this_thread::sleep_for(milliseconds(5));
assert(!cp0_process_group::exists(leader));
errno = 0;
assert(waitpid(-leader, nullptr, WNOHANG) == -1 && errno == ECHILD);
}
2 changes: 1 addition & 1 deletion projects/AppStore
2 changes: 2 additions & 0 deletions projects/ZClaw/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ The available presets are:

For a custom provider, the API URL must begin with `http://` or `https://`. Quickstart uses the agent alias `zclaw` unless the saved UI configuration contains another alias.

Model Settings saves the selected provider and each API URL, API key, or model value as soon as the entry is confirmed with `Enter`. Confirmed values are restored the next time ZClaw starts, even if Quickstart has not been run.

The generated ZeroClaw configuration binds the gateway to `127.0.0.1:42617`, requires pairing, uses a 180-second request timeout, and allows 600 seconds for long-running requests. The UI endpoint is reset to `http://127.0.0.1:42617/webhook`.

## Chat
Expand Down
Loading
Loading