fix: prevent black screen after application upgrades

Serialize service operations and finish migration before starting services.
Install the startup script atomically, remove recursive restarts, and keep
the migration marker until required changes succeed.

Claim video ownership before clearing orphan VENC/VPSS resources, always
finish recovery SYS cleanup, and retain resource state on teardown failures.
Rebuild the bundled libkvm_mmf.so with these fixes.

Validated cross-builds, ABI and recovery failure paths, plus a 2.4.3 to 2.5.3
upgrade on hardware with 1080p browser WebRTC and MJPEG playback.
This commit is contained in:
916BGAI
2026-11-04 19:45:09 +08:00
parent 4b54fc4768
commit 44c420e28b
6 changed files with 199 additions and 135 deletions

View File

@@ -1,6 +1,9 @@
#!/bin/sh #!/bin/sh
# nanokvm Rev3.1 # nanokvm Rev3.1
exec 9>/run/nanokvm-service.lock || exit 1
flock -n 9 || { echo "NanoKVM service operation already in progress" >&2; exit 1; }
wait_for_exit() { wait_for_exit() {
process="$1" process="$1"
timeout="$2" timeout="$2"
@@ -34,43 +37,42 @@ stop_process() {
fi fi
killall -KILL "$process" 2>/dev/null || true killall -KILL "$process" 2>/dev/null || true
wait_for_exit "$process" 2 || true wait_for_exit "$process" 2 || {
echo "Failed to stop $process" >&2
return 1
}
} }
stop_services() { stop_services() {
# The server owns VI/VPSS. Let it release MMF before stopping kvm_system. # The server owns VI/VPSS. Let it release MMF before stopping kvm_system.
stop_process NanoKVM-Server stop_process NanoKVM-Server || return 1
stop_process kvm_system stop_process kvm_system || return 1
rm -rf /tmp/kvm_system /tmp/server
} }
start_services() { start_services() {
cp -r /kvmapp/kvm_system /tmp/ if pidof NanoKVM-Server >/dev/null 2>&1 && pidof kvm_system >/dev/null 2>&1; then
return 0
fi
if pidof NanoKVM-Server >/dev/null 2>&1 || pidof kvm_system >/dev/null 2>&1; then
echo "NanoKVM is partially running; use restart" >&2
return 1
fi
rm -rf /tmp/kvm_system /tmp/server || return 1
cp -r /kvmapp/kvm_system /tmp/ || return 1
if [ -f /kvmapp/kvm_new_app ]; then if [ -f /kvmapp/kvm_new_app ]; then
touch /tmp/.nanokvm_migrating /tmp/kvm_system/kvm_system --migrate 9>&-
/tmp/kvm_system/kvm_system & case "$?" in
0) ;;
# The legacy migration invokes this init script recursively and starts 2) sync; reboot; return "$?" ;;
# the server itself. Wait for that single migration path to finish. *) echo "NanoKVM migration failed" >&2; return 1 ;;
migration_wait=0 esac
while [ "$migration_wait" -lt 30 ]; do
if [ ! -f /kvmapp/kvm_new_app ] && pidof NanoKVM-Server >/dev/null 2>&1; then
rm -f /tmp/.nanokvm_migrating
return
fi
sleep 1
migration_wait=$((migration_wait + 1))
done
rm -f /tmp/.nanokvm_migrating
else
/tmp/kvm_system/kvm_system &
fi fi
if ! pidof NanoKVM-Server >/dev/null 2>&1; then cp -r /kvmapp/server /tmp/ || return 1
rm -rf /tmp/server # Daemons must not inherit the service-operation lock.
cp -r /kvmapp/server /tmp/ /tmp/kvm_system/kvm_system 9>&- &
/tmp/server/NanoKVM-Server & /tmp/server/NanoKVM-Server 9>&- &
fi
} }
case "$1" in case "$1" in
@@ -118,20 +120,17 @@ case "$1" in
fi fi
# Start services # Start services
start_services start_services || exit 1
;; ;;
stop) stop)
stop_services stop_services || exit 1
echo "OK" echo "OK"
;; ;;
restart) restart)
if [ -f /tmp/.nanokvm_migrating ]; then stop_services || exit 1
exit 0 start_services || exit 1
fi
stop_services
start_services
sync sync
echo "OK" echo "OK"

Binary file not shown.

View File

@@ -5,12 +5,19 @@
#include "sample_comm.h" #include "sample_comm.h"
#include <algorithm> #include <algorithm>
#include <cerrno>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <fcntl.h>
#include <limits> #include <limits>
#include <mutex> #include <mutex>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <time.h> #include <time.h>
#include <unistd.h>
#include <linux/cvi_vc_drv_ioctl.h>
namespace kvm_mmf { namespace kvm_mmf {
namespace { namespace {
@@ -37,6 +44,8 @@ SAMPLE_VI_CONFIG_S vi_config = {};
bool system_ready = false, group_created = false, group_started = false; bool system_ready = false, group_created = false, group_started = false;
bool vi_bound = false, vi_started = false; bool vi_bound = false, vi_started = false;
uint64_t next_token[2] = {}, last_pts[2] = {}; uint64_t next_token[2] = {}, last_pts[2] = {};
int owner_fd = -1; // Held until process exit, including across HDMI restarts.
bool stale_resources_cleared = false;
int channel_id(Codec codec) { return codec == H264 ? 0 : 1; } int channel_id(Codec codec) { return codec == H264 ? 0 : 1; }
bool valid_codec(Codec codec) { return codec == H264 || codec == MJPEG; } bool valid_codec(Codec codec) { return codec == H264 || codec == MJPEG; }
@@ -48,6 +57,64 @@ bool check(int result, const char *operation)
return false; return false;
} }
bool unbind_source(MOD_ID_E module, int device, int channel)
{
MMF_CHN_S source = {module, device, channel};
MMF_BIND_DEST_S destinations = {};
if (!check(CVI_SYS_GetBindbySrc(&source, &destinations), "GetBindbySrc")) return false;
for (unsigned i = 0; i < destinations.u32Num; ++i) {
if (!check(CVI_SYS_UnBind(&source, &destinations.astMmfChn[i]), "UnBind stale stream")) return false;
}
return true;
}
bool clear_stale_encoder(int channel)
{
char device[32];
snprintf(device, sizeof(device), "/dev/%s%d", CVI_VC_DRV_ENCODER_DEV_NAME, channel);
int fd = open(device, O_RDWR | O_CLOEXEC);
if (fd < 0) return check(CVI_FAILURE, "open stale encoder");
VENC_CHN_ATTR_S attr = {};
int result = ioctl(fd, CVI_VC_VENC_GET_CHN_ATTR, &attr);
if (result == CVI_SUCCESS) {
fprintf(stderr, "[kvm_mmf] clearing stale VENC channel %d\n", channel);
// The SDK wrapper counts channels created in this process. Its
// DestroyChn cannot adopt an orphan without corrupting that count.
// Use the SDK's kernel ABI only for recovery of the previous owner.
result = ioctl(fd, CVI_VC_VENC_STOP_RECV_FRAME);
if (result == CVI_SUCCESS) result = ioctl(fd, CVI_VC_VENC_RESET_CHN);
if (result == CVI_SUCCESS) result = ioctl(fd, CVI_VC_VENC_DESTROY_CHN);
} else if (result == CVI_ERR_VENC_UNEXIST) {
result = CVI_SUCCESS;
}
close(fd);
return check(result, "clear stale VENC channel");
}
bool clear_stale_resources()
{
if (!unbind_source(CVI_ID_VI, 0, 0)) return false;
for (int channel = 0; channel < VPSS_MAX_CHN_NUM; ++channel) {
if (!unbind_source(CVI_ID_VPSS, group, channel)) return false;
}
for (int channel = 0; channel < 2; ++channel) {
if (!clear_stale_encoder(channel)) return false;
}
VPSS_GRP_ATTR_S attr = {};
int result = CVI_VPSS_GetGrpAttr(group, &attr);
if (result == CVI_SUCCESS) {
fprintf(stderr, "[kvm_mmf] clearing stale VPSS group %d\n", group);
for (int channel = 0; channel < VPSS_MAX_CHN_NUM; ++channel) {
if (!check(CVI_VPSS_DisableChn(group, channel), "Disable stale VPSS channel")) return false;
}
if (!check(CVI_VPSS_StopGrp(group), "Stop stale VPSS group") ||
!check(CVI_VPSS_DestroyGrp(group), "Destroy stale VPSS group")) return false;
} else if (result != CVI_ERR_VPSS_UNEXIST) {
return check(result, "Get stale VPSS group");
}
return true;
}
bool release_locked(Codec codec) bool release_locked(Codec codec)
{ {
Channel &ch = channels[codec]; Channel &ch = channels[codec];
@@ -121,26 +188,55 @@ bool configure_output(Codec codec, uint16_t width, uint16_t height, uint16_t fps
return true; return true;
} }
void deinitialize_locked() bool deinitialize_locked()
{ {
stop_locked(H264); bool h264_stopped = stop_locked(H264);
stop_locked(MJPEG); bool mjpeg_stopped = stop_locked(MJPEG);
if (vi_bound) check(SAMPLE_COMM_VI_UnBind_VPSS(0, 0, group), "VI_UnBind_VPSS"); if (!h264_stopped || !mjpeg_stopped) return false;
if (vi_started) { if (vi_bound) {
check(SAMPLE_COMM_VI_DestroyIsp(&vi_config), "DestroyIsp"); if (!check(SAMPLE_COMM_VI_UnBind_VPSS(0, 0, group), "VI_UnBind_VPSS")) return false;
check(SAMPLE_COMM_VI_DestroyVi(&vi_config), "DestroyVi"); vi_bound = false;
} }
if (group_started) check(CVI_VPSS_StopGrp(group), "StopGrp"); if (vi_started) {
if (group_created) check(CVI_VPSS_DestroyGrp(group), "DestroyGrp"); SAMPLE_COMM_VI_DestroyIsp(&vi_config);
if (system_ready) SAMPLE_COMM_SYS_Exit(); SAMPLE_COMM_VI_DestroyVi(&vi_config);
system_ready = group_created = group_started = vi_bound = vi_started = false; vi_started = false;
channels[H264] = Channel{}; }
channels[MJPEG] = Channel{}; if (group_started) {
if (!check(CVI_VPSS_StopGrp(group), "StopGrp")) return false;
group_started = false;
}
if (group_created) {
if (!check(CVI_VPSS_DestroyGrp(group), "DestroyGrp")) return false;
group_created = false;
}
if (system_ready) {
if (!check(CVI_SYS_Exit(), "SYS_Exit") || !check(CVI_VB_Exit(), "VB_Exit")) return false;
system_ready = false;
}
return true;
} }
bool initialize_locked() bool initialize_locked()
{ {
if (vi_started) return true; if (vi_started) return true;
if (owner_fd < 0) {
int fd = open("/run/nanokvm-video.lock", O_CREAT | O_RDWR | O_CLOEXEC, 0600);
if (fd < 0 || flock(fd, LOCK_EX | LOCK_NB) != 0) {
fprintf(stderr, "[kvm_mmf] cannot acquire video ownership: %s\n", strerror(errno));
if (fd >= 0) close(fd);
return false;
}
owner_fd = fd;
}
if (!stale_resources_cleared) {
if (!check(CVI_SYS_Init(), "SYS_Init for recovery")) return false;
const bool cleared = clear_stale_resources();
const bool exited = check(CVI_SYS_Exit(), "SYS_Exit after recovery");
if (!cleared || !exited) return false;
stale_resources_cleared = true;
}
if (system_ready && !deinitialize_locked()) return false;
SAMPLE_INI_CFG_S ini = {}; SAMPLE_INI_CFG_S ini = {};
PIC_SIZE_E picture; PIC_SIZE_E picture;
SIZE_S size = {}; SIZE_S size = {};
@@ -278,7 +374,7 @@ bool initialize()
bool restart() bool restart()
{ {
std::scoped_lock lock(channel_mutex[H264], channel_mutex[MJPEG]); std::scoped_lock lock(channel_mutex[H264], channel_mutex[MJPEG]);
deinitialize_locked(); if (!deinitialize_locked()) return false;
return initialize_locked(); return initialize_locked();
} }

View File

@@ -10,14 +10,14 @@ using namespace maix::sys;
extern kvm_sys_state_t kvm_sys_state; extern kvm_sys_state_t kvm_sys_state;
extern kvm_oled_state_t kvm_oled_state; extern kvm_oled_state_t kvm_oled_state;
static void write_hdmi_state(uint8_t active) static bool write_hdmi_state(uint8_t active)
{ {
char temp_path[] = "/kvmapp/kvm/.state.init.XXXXXX"; char temp_path[] = "/kvmapp/kvm/.state.init.XXXXXX";
const char *state = active != 0 ? "1\n" : "0\n"; const char *state = active != 0 ? "1\n" : "0\n";
int fd = mkstemp(temp_path); int fd = mkstemp(temp_path);
if(fd < 0){ if(fd < 0){
printf("failed to create HDMI state file: %d\n", errno); printf("failed to create HDMI state file: %d\n", errno);
return; return false;
} }
int failure = 0; int failure = 0;
@@ -40,6 +40,7 @@ static void write_hdmi_state(uint8_t active)
unlink(temp_path); unlink(temp_path);
printf("failed to publish HDMI state file: %d\n", failure); printf("failed to publish HDMI state file: %d\n", failure);
} }
return failure == 0;
} }
uint8_t get_hdmi_version() uint8_t get_hdmi_version()
@@ -88,38 +89,45 @@ void Production_testing_patch(void)
system("sync"); system("sync");
} }
void new_app_init(void) static bool init_command(const char *command)
{
if(system(command) == 0) return true;
fprintf(stderr, "[kvms] migration failed: %s\n", command);
return false;
}
AppInitResult new_app_init(void)
{ {
// Update the necessary scripts // Update the necessary scripts
system("rm -f /boot/logo.jpeg"); system("rm -f /boot/logo.jpeg");
system("cp -f /kvmapp/system/update-nanokvm.py /etc/kvm/"); if(!init_command("mkdir -p /etc/kvm /kvmapp/kvm")) return APP_INIT_FAILED;
if(!init_command("cp -f /kvmapp/system/update-nanokvm.py /etc/kvm/")) return APP_INIT_FAILED;
system("rm -f /etc/init.d/S02udisk"); system("rm -f /etc/init.d/S02udisk");
system("cp -f /kvmapp/system/init.d/S00kmod /etc/init.d/"); if(!init_command("cp -f /kvmapp/system/init.d/S00kmod /etc/init.d/")) return APP_INIT_FAILED;
system("cp -f /kvmapp/system/init.d/S01fs /etc/init.d/"); if(!init_command("cp -f /kvmapp/system/init.d/S01fs /etc/init.d/")) return APP_INIT_FAILED;
system("cp -f /kvmapp/system/init.d/S03usbdev /etc/init.d/"); if(!init_command("cp -f /kvmapp/system/init.d/S03usbdev /etc/init.d/")) return APP_INIT_FAILED;
system("cp -f /kvmapp/system/init.d/S15kvmhwd /etc/init.d/"); if(!init_command("cp -f /kvmapp/system/init.d/S15kvmhwd /etc/init.d/")) return APP_INIT_FAILED;
system("cp -f /kvmapp/system/init.d/S30eth /etc/init.d/"); if(!init_command("cp -f /kvmapp/system/init.d/S30eth /etc/init.d/")) return APP_INIT_FAILED;
system("cp -f /kvmapp/system/init.d/S50sshd /etc/init.d/"); if(!init_command("cp -f /kvmapp/system/init.d/S50sshd /etc/init.d/")) return APP_INIT_FAILED;
if(kvm_wifi_exist()) { if(kvm_wifi_exist()) {
system("cp -f /kvmapp/system/init.d/S30wifi /etc/init.d/"); if(!init_command("cp -f /kvmapp/system/init.d/S30wifi /etc/init.d/")) return APP_INIT_FAILED;
} else { } else {
system("rm -f /etc/init.d/S30wifi"); system("rm -f /etc/init.d/S30wifi");
} }
// if exit /etc/init.d/S98tailscaled then cp -f /kvmapp/system/init.d/S98tailscaled /etc/init.d/ // if exit /etc/init.d/S98tailscaled then cp -f /kvmapp/system/init.d/S98tailscaled /etc/init.d/
if(access("/etc/init.d/S98tailscaled", F_OK) == 0){ if(access("/etc/init.d/S98tailscaled", F_OK) == 0){
system("cp -f /kvmapp/system/init.d/S98tailscaled /etc/init.d/"); if(!init_command("cp -f /kvmapp/system/init.d/S98tailscaled /etc/init.d/")) return APP_INIT_FAILED;
} }
// rmmod soph_saradc // rmmod soph_saradc
system("rmmod soph_saradc"); system("rmmod soph_saradc");
system("rm -f /mnt/system/ko/soph_saradc.ko"); system("rm -f /mnt/system/ko/soph_saradc.ko");
// PCIe Patch // Keep an already running legacy shell on its original script inode.
// system("cp /kvmapp/system/init.d/S95nanokvm /etc/init.d/"); if(!init_command("cp /kvmapp/system/init.d/S95nanokvm /etc/init.d/.S95nanokvm.new && "
if(access("/kvmapp/jpg_stream/dl_lib/libmaixcam_lib.so", F_OK) != 0){ "chmod 755 /etc/init.d/.S95nanokvm.new && "
system("cp -f /kvmapp/system/init.d/S95nanokvm /etc/init.d/"); "mv -f /etc/init.d/.S95nanokvm.new /etc/init.d/S95nanokvm")) return APP_INIT_FAILED;
}
// Remove unnecessary components to speed up boot time // Remove unnecessary components to speed up boot time
system("rm -f /etc/init.d/S04backlight"); system("rm -f /etc/init.d/S04backlight");
@@ -129,90 +137,45 @@ void new_app_init(void)
system("rm -f /etc/init.d/S99*"); system("rm -f /etc/init.d/S99*");
// Add necessary configuration files for program execution // Add necessary configuration files for program execution
system("mkdir /kvmapp/kvm"); if(!init_command("echo 0 > /kvmapp/kvm/now_fps && echo 30 > /kvmapp/kvm/fps && "
system("mkdir /etc/kvm"); "echo 2000 > /kvmapp/kvm/qlty && echo h264 > /kvmapp/kvm/type && "
system("echo 0 > /kvmapp/kvm/now_fps"); "touch /etc/kvm/frame_detact")) return APP_INIT_FAILED;
system("echo 30 > /kvmapp/kvm/fps"); if(!write_hdmi_state(0)) return APP_INIT_FAILED;
system("echo 2000 > /kvmapp/kvm/qlty");
system("echo h264 > /kvmapp/kvm/type");
write_hdmi_state(0);
system("touch /etc/kvm/frame_detact");
// rm jpg_stream & kvm_stream // rm jpg_stream & kvm_stream
system("rm -rf /kvmapp/jpg_stream"); system("rm -rf /kvmapp/jpg_stream");
system("rm -f /kvmapp/kvm_system/kvm_stream"); // Cannot delete temporarily; the old production test script uses this file to determine if the download is complete system("rm -f /kvmapp/kvm_system/kvm_stream"); // Cannot delete temporarily; the old production test script uses this file to determine if the download is complete
system("rm -f /kvmapp/kvm_new_app");
system("sync");
// system("/etc/init.d/S95nanokvm restart");
// update ko // update ko
FILE *fp; FILE *fp = popen("md5sum /mnt/system/ko/soph_mipi_rx.ko", "r");
uint8_t RW_Data_0[30]; if(!fp) return APP_INIT_FAILED;
uint8_t RW_Data_1[30]; char digest[33] = {};
fp = popen("md5sum /mnt/system/ko/soph_mipi_rx.ko | grep 086ed01749188975afaa40fb569374f8 | awk '{print $2}'", "r"); bool digest_read = fscanf(fp, "%32s", digest) == 1;
if ( NULL == fp ) int digest_status = pclose(fp);
{ if(!digest_read || digest_status != 0) return APP_INIT_FAILED;
pclose(fp);
// return;
}
fgets((char*)RW_Data_0, 10, fp);
pclose(fp);
fp = popen("md5sum /mnt/system/ko/soph_mipi_rx.ko | grep 69be7eeded3777f750480a5dd5a1aa26 | awk '{print $2}'", "r");
if ( NULL == fp )
{
pclose(fp);
// return;
}
fgets((char*)RW_Data_1, 10, fp);
pclose(fp);
int8_t hdmi_ver = -1; int8_t hdmi_ver = -1;
if(access("/etc/kvm/hdmi_version", F_OK) == 0){ if(access("/etc/kvm/hdmi_version", F_OK) == 0){
uint8_t RW_Data[2];
FILE *fp = fopen("/etc/kvm/hdmi_version", "r"); FILE *fp = fopen("/etc/kvm/hdmi_version", "r");
fread(RW_Data, sizeof(char), 2, fp); if(!fp) return APP_INIT_FAILED;
int version = fgetc(fp);
fclose(fp); fclose(fp);
if(RW_Data[0] == 'c') hdmi_ver = 1; if(version == EOF) return APP_INIT_FAILED;
else if(RW_Data[0] == 'u') hdmi_ver = 2; if(version == 'c') hdmi_ver = 1;
else if(RW_Data[0] == 'd') hdmi_ver = 2; else if(version == 'u' || version == 'd') hdmi_ver = 2;
} }
// system("/etc/init.d/S03usbdev stop_start");
create_temp_watchdog(); create_temp_watchdog();
bool current_driver = strcmp(digest, "69be7eeded3777f750480a5dd5a1aa26") == 0 ||
if(hdmi_ver == 2){ (hdmi_ver != 2 && strcmp(digest, "086ed01749188975afaa40fb569374f8") == 0);
if(RW_Data_1[0] != '/'){ if(!current_driver){
system("cp /kvmapp/system/ko/soph_mipi_rx.ko /mnt/system/ko/soph_mipi_rx.ko"); if(!init_command("cp /kvmapp/system/ko/soph_mipi_rx.ko /mnt/system/ko/soph_mipi_rx.ko")) return APP_INIT_FAILED;
system("sync");
system("reboot");
} else {
system("sync");
system("/etc/init.d/S15kvmhwd start");
system("/etc/init.d/S95nanokvm restart");
}
} else {
if((RW_Data_0[0] != '/') && (RW_Data_1[0] != '/')){
system("cp /kvmapp/system/ko/soph_mipi_rx.ko /mnt/system/ko/soph_mipi_rx.ko");
system("sync");
system("reboot");
} else {
system("sync");
system("/etc/init.d/S15kvmhwd start");
system("/etc/init.d/S95nanokvm restart");
}
} }
if(access("/root/old/kvm_new_img", F_OK) == 0){ if(unlink("/kvmapp/kvm_new_app") != 0) return APP_INIT_FAILED;
system("rm -r /root/old"); sync();
} return current_driver ? APP_INIT_OK : APP_INIT_REBOOT;
system("sync");
system("killall NanoKVM-Server");
system("rm -r /tmp/server");
system("cp -r /kvmapp/server /tmp/");
system("/tmp/server/NanoKVM-Server &");
} }
void build_complete_resolv(void) void build_complete_resolv(void)

View File

@@ -3,7 +3,8 @@
#include "config.h" #include "config.h"
void new_img_init(void); void new_img_init(void);
void new_app_init(void); enum AppInitResult { APP_INIT_OK = 0, APP_INIT_FAILED = 1, APP_INIT_REBOOT = 2 };
AppInitResult new_app_init(void);
void Production_testing_patch(void); void Production_testing_patch(void);
#endif // SYSTEM_INIT_H_ #endif // SYSTEM_INIT_H_

View File

@@ -192,10 +192,15 @@ int main(int argc, char* argv[])
new_img_init(); new_img_init();
} }
// Execute only when it's a new app const bool migrate_only = argc == 2 && strcmp(argv[1], "--migrate") == 0;
if(access("/kvmapp/kvm_new_app", F_OK) == 0){ if(access("/kvmapp/kvm_new_app", F_OK) == 0){
new_app_init(); AppInitResult result = new_app_init();
if(migrate_only || result == APP_INIT_FAILED) return result;
// A legacy init script starts us without --migrate and can still be
// copying the server. Boot once into the newly installed init script.
return system("reboot") == 0 ? 0 : 1;
} }
if(migrate_only) return APP_INIT_OK;
// only for production testing // only for production testing
if (access("/tmp/S49kvmtest", F_OK) == 0){ if (access("/tmp/S49kvmtest", F_OK) == 0){