From f644cfa7822ee9fc3c57d4b6788bc72dce58e86a Mon Sep 17 00:00:00 2001 From: fjr Date: Thu, 22 Oct 2026 08:48:39 +0000 Subject: [PATCH] qmodem: Gate dialing on current device presence and stop stale slots --- .github/workflows/uplink-runtime.yml | 1 + .../modem_scan/src/modem_scan_common.h | 2 + application/modem_scan/src/modem_scanc.c | 5 +- application/modem_scan/src/modem_scand.c | 276 ++++++++++++++---- application/modem_scan/tests/test_presence.c | 113 +++++++ .../files/etc/hotplug.d/net/20-modem-net | 2 - .../files/etc/hotplug.d/usb/20-modem-usb | 2 - .../qmodem/files/etc/init.d/qmodem_init | 39 +-- .../qmodem/files/etc/init.d/qmodem_network | 36 ++- .../files/usr/share/qmodem/modem_dial.sh | 40 ++- .../files/usr/share/qmodem/modem_ready.sh | 69 +++++ .../files/usr/share/qmodem/modem_util.sh | 4 + .../qmodem/tests/test_modem_presence.sh | 177 +++++++++++ docs/Dev_guide_modem_scan_workflow.md | 35 ++- 14 files changed, 684 insertions(+), 117 deletions(-) create mode 100644 application/modem_scan/tests/test_presence.c create mode 100755 application/qmodem/files/usr/share/qmodem/modem_ready.sh create mode 100755 application/qmodem/tests/test_modem_presence.sh diff --git a/.github/workflows/uplink-runtime.yml b/.github/workflows/uplink-runtime.yml index 01cce8229..3f4ac8279 100644 --- a/.github/workflows/uplink-runtime.yml +++ b/.github/workflows/uplink-runtime.yml @@ -20,6 +20,7 @@ jobs: sh application/qmodem_uplink_manager/tests/test_health.sh sh application/qmodem/tests/test_bridge_passthrough.sh sh application/qmodem/tests/test_modem_netdev.sh + sh application/qmodem/tests/test_modem_presence.sh cc -pthread application/modem_scan/tests/test_scheduler.c -ljson-c -o /tmp/test-scheduler /tmp/test-scheduler cc -pthread application/quectel_CM_5G_M/tests/test_managed_ipv4.c -o /tmp/test-managed diff --git a/application/modem_scan/src/modem_scan_common.h b/application/modem_scan/src/modem_scan_common.h index f62aee4f0..7d4748e58 100644 --- a/application/modem_scan/src/modem_scan_common.h +++ b/application/modem_scan/src/modem_scan_common.h @@ -2,7 +2,9 @@ #define QMODEM_MODEM_SCAN_COMMON_H #define QMODEM_SCAND_SOCKET "/var/run/qmodem/modem_scand.sock" +#ifndef QMODEM_RUN_DIR #define QMODEM_RUN_DIR "/var/run/qmodem" +#endif #define QMODEM_SUPPORT_JSON "/usr/share/qmodem/modem_support.json" #define QMODEM_PORT_RULE_JSON "/usr/share/qmodem/modem_port_rule.json" diff --git a/application/modem_scan/src/modem_scanc.c b/application/modem_scan/src/modem_scanc.c index c736c19df..2232ff9a0 100644 --- a/application/modem_scan/src/modem_scanc.c +++ b/application/modem_scan/src/modem_scanc.c @@ -17,6 +17,7 @@ static void usage(FILE *out) " modem_scanc disable [delay_seconds]\n" " modem_scanc scan [usb|pcie|all] [delay_seconds]\n" " modem_scanc set-log-level \n" + " modem_scanc check
\n" " modem_scanc status\n"); } @@ -78,12 +79,12 @@ int main(int argc, char **argv) return 1; } snprintf(line, sizeof(line), "add %s %s %s", argv[2], argv[3], argc == 5 ? argv[4] : "0"); - } else if (!strcmp(argv[1], "remove")) { + } else if (!strcmp(argv[1], "remove") || !strcmp(argv[1], "check")) { if (argc != 3 && argc != 4) { usage(stderr); return 1; } - snprintf(line, sizeof(line), "remove %s %s", argv[2], argc == 4 ? argv[3] : "0"); + snprintf(line, sizeof(line), "%s %s %s", argv[1], argv[2], argc == 4 ? argv[3] : "0"); } else if (!strcmp(argv[1], "disable")) { if (argc != 3 && argc != 4) { usage(stderr); diff --git a/application/modem_scan/src/modem_scand.c b/application/modem_scan/src/modem_scand.c index 90448264f..a092eb2e1 100644 --- a/application/modem_scan/src/modem_scand.c +++ b/application/modem_scan/src/modem_scand.c @@ -22,11 +22,22 @@ #include #include +#ifndef QMODEM_READY_HELPER +#define QMODEM_READY_HELPER "/usr/share/qmodem/modem_ready.sh" +#endif +#ifndef QMODEM_NETWORK_INIT +#define QMODEM_NETWORK_INIT "/etc/init.d/qmodem_network" +#endif +#ifndef QMODEM_CONFIG_LOCK +#define QMODEM_CONFIG_LOCK "/var/lock/qmodem-config.lock" +#endif + enum event_type { EV_ADD, EV_REMOVE, EV_DISABLE, EV_SCAN, + EV_CHECK, }; enum log_level { @@ -126,7 +137,7 @@ static void log_msg(enum log_level level, const char *fmt, ...); static void config_lock(void) { pthread_mutex_lock(&uci_lock); mkdir("/var/lock", 0755); - config_fd = open("/var/lock/qmodem-config.lock", O_CREAT|O_RDWR|O_CLOEXEC, 0600); + config_fd = open(QMODEM_CONFIG_LOCK, O_CREAT|O_RDWR|O_CLOEXEC, 0600); if (config_fd < 0 || flock(config_fd, LOCK_EX)) { log_msg(LOG_L_ERR, "cannot acquire config transaction lock"); abort(); @@ -401,10 +412,10 @@ static int uci_add_list(const char *key, const char *value) return run_exec(argv, 3); } -static void uci_commit(const char *config) +static int uci_commit(const char *config) { char *argv[] = { "uci", "commit", (char *)config, NULL }; - run_exec(argv, 5); + return run_exec(argv, 5); } static int find_slot_section(const char *slot, char *section, size_t len) @@ -1071,18 +1082,109 @@ static void exec_post_init(const char *section) static void reload_network(void) { - char *argv[] = { "/etc/init.d/qmodem_network", "reload", NULL }; + char *argv[] = { QMODEM_NETWORK_INIT, "reload", NULL }; run_exec(argv, 60); } +/* Only the scanner publishes presence; the dial service consumes it. */ +static void revoke_ready(const char *section) +{ + char path[256]; + snprintf(path, sizeof(path), QMODEM_RUN_DIR "/%s_dir/ready", section); + unlink(path); +} + +static int ready_command(const char *action, const char *section) +{ + char *argv[] = { QMODEM_READY_HELPER, (char *)action, (char *)section, NULL }; + return run_exec(argv, 10); +} + +static int disable_section(const char *section) +{ + char key[256], existing[64], state[32]; + int rc = 0; + pthread_mutex_lock(&g.queue_lock); + if (!event_current()) { pthread_mutex_unlock(&g.queue_lock); return 0; } + revoke_ready(section); + config_lock(); + snprintf(key, sizeof(key), "qmodem.%s", section); + if (uci_get(key, existing, sizeof(existing)) || strcmp(existing, "modem-device")) { + config_unlock(); pthread_mutex_unlock(&g.queue_lock); return 0; + } + snprintf(key, sizeof(key), "qmodem.%s.state", section); + uci_get(key, state, sizeof(state)); + if (strcmp(state, "disabled")) rc = uci_set(key, "disabled"); + rc |= uci_commit("qmodem"); + config_unlock(); + pthread_mutex_unlock(&g.queue_lock); + char *stop[] = { QMODEM_NETWORK_INIT, "offline", (char *)section, NULL }; + rc |= run_exec(stop, 60); + log_msg(rc ? LOG_L_ERR : LOG_L_INFO, "disable section=%s %s", section, + rc ? "incomplete" : "host cleanup complete"); + return rc; +} + +static int confirm_section(const char *section) +{ + char key[256], state[32]; + int rc; + pthread_mutex_lock(&g.queue_lock); + if (!event_current()) { pthread_mutex_unlock(&g.queue_lock); return 0; } + config_lock(); + rc = ready_command("confirm", section); + if (!rc) { + snprintf(key, sizeof(key), "qmodem.%s.state", section); + uci_get(key, state, sizeof(state)); + if (strcmp(state, "enabled")) rc = uci_set(key, "enabled"); + rc |= uci_commit("qmodem"); + } + if (rc) revoke_ready(section); + config_unlock(); + pthread_mutex_unlock(&g.queue_lock); + return rc; +} + +static int check_modem(const char *section) +{ + char key[256], existing[64]; + snprintf(key, sizeof(key), "qmodem.%s", section); + if (uci_get(key, existing, sizeof(existing)) || strcmp(existing, "modem-device")) return 0; + if (!ready_command("check", section)) return 0; + if (disable_section(section)) return 1; + if (confirm_section(section)) return 1; + reload_network(); + return 0; +} + +static void check_configured(const char *type) +{ + char out[32768], *save = NULL, *line; + char *argv[] = { "uci", "-q", "show", "qmodem", NULL }; + if (capture_exec(argv, out, sizeof(out), 5)) return; + for (line = strtok_r(out, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) { + char *end, key[256], interface[32]; + if (strncmp(line, "qmodem.", 7)) continue; + end = strchr(line + 7, '='); + if (!end || strcmp(end, "=modem-device")) continue; + *end = 0; + const char *section = line + 7; + if (!*section || strspn(section, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_") != strlen(section)) continue; + snprintf(key, sizeof(key), "qmodem.%s.data_interface", section); + uci_get(key, interface, sizeof(interface)); + if (strcmp(type, "all") && strcmp(type, interface)) continue; + enqueue_event(EV_CHECK, section, NULL, 0); + } +} + static int add_modem(const char *slot, const char *slot_type) { struct scan_result res; struct modem_profile profile; - char section[128], key[256], existing[64], fixed[16]; + char section[128], key[256], existing[64], fixed[16], manual[16]; char orig_network[512] = "", orig_at[128] = "", orig_state[64] = "", orig_name[128] = ""; char net_join[512], default_alias[128] = "", default_metric[32] = ""; - int existed; + int existed, write_rc = 0, changed; memset(&res, 0, sizeof(res)); sl_init(&res.net_devices); @@ -1091,13 +1193,17 @@ static int add_modem(const char *slot, const char *slot_type) sl_init(&res.valid_at_ports); section_from_slot(slot, section, sizeof(section)); - snprintf(key, sizeof(key), "qmodem.%s.fixed_device", section); - uci_get(key, fixed, sizeof(fixed)); - if (!strcmp(fixed, "1")) { - log_msg(LOG_L_INFO, "skip fixed device slot=%s section=%s", slot, section); - exec_post_init(section); - goto out_success; - } + uci_get("qmodem.main.block_auto_probe", manual, sizeof(manual)); + snprintf(key, sizeof(key), "qmodem.%s.fixed_device", section); + uci_get(key, fixed, sizeof(fixed)); + if (!strcmp(fixed, "1") || !strcmp(manual, "1")) { + int rc = check_modem(section); + if (rc) goto out_fail; + if (strcmp(manual, "1") && !ready_command("check", section)) exec_post_init(section); + goto out_success; + } + /* A cached port must not survive a failed rescan or re-enumeration. */ + if (ready_command("check", section) && disable_section(section)) goto out_fail; if (!strcmp(slot_type, "usb")) { scan_usb_slot(slot, &res); @@ -1148,7 +1254,7 @@ static int add_modem(const char *slot, const char *slot_type) snprintf(key, sizeof(key), "qmodem.%s.ports", section); uci_del(key); snprintf(key, sizeof(key), "qmodem.%s.voice_pcm_port", section); uci_del(key); snprintf(key, sizeof(key), "qmodem.%s.voice_pcm_interface", section); uci_del(key); - snprintf(key, sizeof(key), "qmodem.%s.state", section); uci_set(key, "enabled"); + snprintf(key, sizeof(key), "qmodem.%s.state", section); write_rc |= uci_set(key, "disabled"); } else { char modem_count_s[32] = "", metric[32]; int modem_count = 0; @@ -1159,57 +1265,60 @@ static int add_modem(const char *slot, const char *slot_type) modem_count = atoi(modem_count_s); modem_count++; snprintf(modem_count_s, sizeof(modem_count_s), "%d", modem_count); - uci_set("qmodem.main.modem_count", modem_count_s); - snprintf(key, sizeof(key), "qmodem.%s", section); uci_set(key, "modem-device"); - if (default_alias[0] && strcmp(default_alias, "-")) { snprintf(key, sizeof(key), "qmodem.%s.alias", section); uci_set(key, default_alias); } + write_rc |= uci_set("qmodem.main.modem_count", modem_count_s); + snprintf(key, sizeof(key), "qmodem.%s", section); write_rc |= uci_set(key, "modem-device"); + if (default_alias[0] && strcmp(default_alias, "-")) { snprintf(key, sizeof(key), "qmodem.%s.alias", section); write_rc |= uci_set(key, default_alias); } snprintf(metric, sizeof(metric), "%d", modem_count + 10); if (default_metric[0]) snprintf(metric, sizeof(metric), "%s", default_metric); - snprintf(key, sizeof(key), "qmodem.%s.path", section); uci_set(key, res.modem_path); - snprintf(key, sizeof(key), "qmodem.%s.data_interface", section); uci_set(key, slot_type); - snprintf(key, sizeof(key), "qmodem.%s.enable_dial", section); uci_set(key, "1"); - snprintf(key, sizeof(key), "qmodem.%s.use_ubus", section); uci_set(key, "1"); - snprintf(key, sizeof(key), "qmodem.%s.soft_reboot", section); uci_set(key, "1"); - snprintf(key, sizeof(key), "qmodem.%s.extend_prefix", section); uci_set(key, "1"); - snprintf(key, sizeof(key), "qmodem.%s.pdp_type", section); uci_set(key, "ipv4v6"); - snprintf(key, sizeof(key), "qmodem.%s.state", section); uci_set(key, "enabled"); - snprintf(key, sizeof(key), "qmodem.%s.metric", section); uci_set(key, metric); + snprintf(key, sizeof(key), "qmodem.%s.path", section); write_rc |= uci_set(key, res.modem_path); + snprintf(key, sizeof(key), "qmodem.%s.data_interface", section); write_rc |= uci_set(key, slot_type); + snprintf(key, sizeof(key), "qmodem.%s.enable_dial", section); write_rc |= uci_set(key, "1"); + snprintf(key, sizeof(key), "qmodem.%s.use_ubus", section); write_rc |= uci_set(key, "1"); + snprintf(key, sizeof(key), "qmodem.%s.soft_reboot", section); write_rc |= uci_set(key, "1"); + snprintf(key, sizeof(key), "qmodem.%s.extend_prefix", section); write_rc |= uci_set(key, "1"); + snprintf(key, sizeof(key), "qmodem.%s.pdp_type", section); write_rc |= uci_set(key, "ipv4v6"); + snprintf(key, sizeof(key), "qmodem.%s.state", section); write_rc |= uci_set(key, "disabled"); + snprintf(key, sizeof(key), "qmodem.%s.metric", section); write_rc |= uci_set(key, metric); } - snprintf(key, sizeof(key), "qmodem.%s.name", section); uci_set(key, profile.name); - snprintf(key, sizeof(key), "qmodem.%s.network", section); uci_set(key, net_join); - snprintf(key, sizeof(key), "qmodem.%s.manufacturer", section); uci_set(key, profile.manufacturer); - snprintf(key, sizeof(key), "qmodem.%s.platform", section); uci_set(key, profile.platform); - snprintf(key, sizeof(key), "qmodem.%s.suggest_pdp_index", section); uci_set(key, profile.pdp_index); - if (profile.wcdma_band[0]) { snprintf(key, sizeof(key), "qmodem.%s.wcdma_band", section); uci_set(key, profile.wcdma_band); } - if (profile.lte_band[0]) { snprintf(key, sizeof(key), "qmodem.%s.lte_band", section); uci_set(key, profile.lte_band); } - if (profile.nsa_band[0]) { snprintf(key, sizeof(key), "qmodem.%s.nsa_band", section); uci_set(key, profile.nsa_band); } - if (profile.sa_band[0]) { snprintf(key, sizeof(key), "qmodem.%s.sa_band", section); uci_set(key, profile.sa_band); } + snprintf(key, sizeof(key), "qmodem.%s.name", section); write_rc |= uci_set(key, profile.name); + snprintf(key, sizeof(key), "qmodem.%s.network", section); write_rc |= uci_set(key, net_join); + snprintf(key, sizeof(key), "qmodem.%s.manufacturer", section); write_rc |= uci_set(key, profile.manufacturer); + snprintf(key, sizeof(key), "qmodem.%s.platform", section); write_rc |= uci_set(key, profile.platform); + snprintf(key, sizeof(key), "qmodem.%s.suggest_pdp_index", section); write_rc |= uci_set(key, profile.pdp_index); + if (profile.wcdma_band[0]) { snprintf(key, sizeof(key), "qmodem.%s.wcdma_band", section); write_rc |= uci_set(key, profile.wcdma_band); } + if (profile.lte_band[0]) { snprintf(key, sizeof(key), "qmodem.%s.lte_band", section); write_rc |= uci_set(key, profile.lte_band); } + if (profile.nsa_band[0]) { snprintf(key, sizeof(key), "qmodem.%s.nsa_band", section); write_rc |= uci_set(key, profile.nsa_band); } + if (profile.sa_band[0]) { snprintf(key, sizeof(key), "qmodem.%s.sa_band", section); write_rc |= uci_set(key, profile.sa_band); } for (size_t i = 0; i < profile.modes.len; i++) { snprintf(key, sizeof(key), "qmodem.%s.modes", section); - uci_add_list(key, profile.modes.items[i]); + write_rc |= uci_add_list(key, profile.modes.items[i]); } for (size_t i = 0; i < res.valid_at_ports.len; i++) { snprintf(key, sizeof(key), "qmodem.%s.valid_at_ports", section); - uci_add_list(key, res.valid_at_ports.items[i]); + write_rc |= uci_add_list(key, res.valid_at_ports.items[i]); } snprintf(key, sizeof(key), "qmodem.%s.at_port", section); - uci_set(key, res.preferred_at[0] ? res.preferred_at : res.valid_at_ports.items[0]); + write_rc |= uci_set(key, res.preferred_at[0] ? res.preferred_at : res.valid_at_ports.items[0]); for (size_t i = 0; i < res.at_ports.len; i++) { snprintf(key, sizeof(key), "qmodem.%s.ports", section); - uci_add_list(key, res.at_ports.items[i]); + write_rc |= uci_add_list(key, res.at_ports.items[i]); } if (res.voice_pcm_port[0]) { snprintf(key, sizeof(key), "qmodem.%s.voice_pcm_port", section); - uci_set(key, res.voice_pcm_port); + write_rc |= uci_set(key, res.voice_pcm_port); snprintf(key, sizeof(key), "qmodem.%s.voice_pcm_interface", section); - uci_set(key, res.voice_pcm_interface); + write_rc |= uci_set(key, res.voice_pcm_interface); } if (res.option_driver) { snprintf(key, sizeof(key), "qmodem.%s.option_driver", section); - uci_set(key, "1"); + write_rc |= uci_set(key, "1"); } - uci_commit("qmodem"); + changed = !existed || strcmp(orig_network, net_join) || strcmp(orig_at, res.preferred_at) || + strcmp(orig_state, "enabled") || strcmp(orig_name, profile.name); + if (changed || write_rc) revoke_ready(section); + int commit_rc = uci_commit("qmodem") | write_rc; config_unlock(); pthread_mutex_unlock(&g.queue_lock); @@ -1219,9 +1328,14 @@ static int add_modem(const char *slot, const char *slot_type) mkdir(QMODEM_RUN_DIR, 0755); mkdir(rundir, 0755); } + if (changed && disable_section(section)) commit_rc = 1; + if (commit_rc || confirm_section(section)) { + disable_section(section); + sl_free(&profile.modes); + goto out_fail; + } exec_post_init(section); - if (!existed || strcmp(orig_network, net_join) || strcmp(orig_at, res.preferred_at) || - strcmp(orig_state, "enabled") || strcmp(orig_name, profile.name)) + if (changed) reload_network(); log_msg(LOG_L_INFO, "added modem section=%s name=%s type=%s ports=%zu valid=%zu", @@ -1249,7 +1363,7 @@ static void remove_modem(const char *section) snprintf(key, sizeof(key), "qmodem.%s", section); if (uci_get(key, existing, sizeof(existing)) || !existing[0]) return; - { char *stop[] = { "/etc/init.d/qmodem_network", "hang", (char *)section, NULL }; + { char *stop[] = { QMODEM_NETWORK_INIT, "hang", (char *)section, NULL }; if (run_exec(stop, 60)) { log_msg(LOG_L_ERR,"refuse remove: could not stop section=%s",section); return; } } config_lock(); uci_get("qmodem.main.modem_count", count_s, sizeof(count_s)); @@ -1267,20 +1381,11 @@ static void remove_modem(const char *section) log_msg(LOG_L_INFO, "removed modem section=%s", section); } -static void disable_slot(const char *slot) +static int disable_slot(const char *slot) { - char section[128], key[256]; - char *argv_reorder[] = { "uci", "-q", "reorder", key, NULL }; - section_from_slot(slot, section, sizeof(section)); - config_lock(); - snprintf(key, sizeof(key), "qmodem.%s=1", section); - run_exec(argv_reorder, 3); - snprintf(key, sizeof(key), "qmodem.%s.state", section); - uci_set(key, "disabled"); - uci_commit("qmodem"); - config_unlock(); - { char *stop[] = { "/etc/init.d/qmodem_network", "hang", section, NULL }; run_exec(stop, 60); } - log_msg(LOG_L_INFO, "disabled slot=%s section=%s", slot, section); + char section[128]; + section_from_slot(slot, section, sizeof(section)); + return disable_section(section); } static void scan_usb_all(void) @@ -1368,6 +1473,7 @@ static int event_key(enum event_type type, const char *a, const char *b, char *o case EV_ADD: return snprintf(out, len, "add:%s:%s", b, a); case EV_REMOVE: return snprintf(out, len, "remove:%s", a); case EV_DISABLE: return snprintf(out, len, "disable:%s", a); + case EV_CHECK: return snprintf(out, len, "check:%s", a); case EV_SCAN: return snprintf(out, len, "scan:%s", a && *a ? a : "all"); } return -1; @@ -1434,6 +1540,11 @@ static int associated_pcie_slot(const char *usb, char *owner, size_t len) static int enqueue_event_ex(enum event_type type, const char *arg, const char *slot_type, int delay_sec, int attempts) { char owner[128]=""; + const char *allowed = (type == EV_CHECK || type == EV_REMOVE) + ? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_" + : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.:-"; + if (!arg || !*arg || strlen(arg) >= 128 || strspn(arg, allowed) != strlen(arg)) return -1; + if (type == EV_ADD && (!slot_type || (strcmp(slot_type, "usb") && strcmp(slot_type, "pcie")))) return -1; if (type == EV_ADD || type == EV_DISABLE) { int associated=associated_pcie_slot(arg,owner,sizeof(owner)); if (associated<0) { log_msg(LOG_L_ERR,"ambiguous associated USB slot=%s",arg); return -1; } @@ -1444,8 +1555,8 @@ static int enqueue_event_ex(enum event_type type, const char *arg, const char *s return -1; e->type = type; if (arg) - snprintf(type == EV_REMOVE ? e->section : e->slot, - type == EV_REMOVE ? sizeof(e->section) : sizeof(e->slot), + snprintf((type == EV_REMOVE || type == EV_CHECK) ? e->section : e->slot, + (type == EV_REMOVE || type == EV_CHECK) ? sizeof(e->section) : sizeof(e->slot), "%s", arg); if (slot_type) snprintf(e->slot_type, sizeof(e->slot_type), "%s", slot_type); @@ -1471,6 +1582,7 @@ static int enqueue_event_ex(enum event_type type, const char *arg, const char *s } if (!attempts) ++*generation; e->generation = *generation; + if (type == EV_DISABLE || type == EV_REMOVE) revoke_ready(e->section); if (!g.queue_head || e->not_before < g.queue_head->not_before) { e->next = g.queue_head; g.queue_head = e; @@ -1575,9 +1687,40 @@ static void process_event(struct event *e) remove_modem(e->section); break; case EV_DISABLE: - disable_slot(e->slot); - break; - case EV_SCAN: + if (disable_slot(e->slot)) { + if (e->attempts < g.add_retry_max) + enqueue_event_ex(EV_DISABLE, e->slot, NULL, g.add_retry_delay, e->attempts + 1); + else + log_msg(LOG_L_ERR, "disable slot=%s exhausted retries; remains unready", e->slot); + } + break; + case EV_CHECK: + if (check_modem(e->section) && e->attempts < g.add_retry_max) { + char key[256], manual[16], fixed[16], path[256], type[16], section[128]; + uci_get("qmodem.main.block_auto_probe", manual, sizeof(manual)); + snprintf(key, sizeof(key), "qmodem.%s.fixed_device", e->section); + uci_get(key, fixed, sizeof(fixed)); + snprintf(key, sizeof(key), "qmodem.%s.path", e->section); + uci_get(key, path, sizeof(path)); + size_t len = strlen(path); + while (len && path[len - 1] == '/') path[--len] = 0; + const char *slot = strrchr(path, '/'); + slot = slot ? slot + 1 : path; + section_from_slot(slot, section, sizeof(section)); + snprintf(key, sizeof(key), "qmodem.%s.data_interface", e->section); + uci_get(key, type, sizeof(type)); + if (strcmp(manual, "1") && strcmp(fixed, "1") && !strcmp(section, e->section) && + (!strcmp(type, "usb") || !strcmp(type, "pcie"))) + enqueue_event_ex(EV_ADD, slot, type, g.add_retry_delay, e->attempts + 1); + else + enqueue_event_ex(EV_CHECK, e->section, NULL, g.add_retry_delay, e->attempts + 1); + } + break; + case EV_SCAN: + check_configured(e->slot); + { char manual[16]; + uci_get("qmodem.main.block_auto_probe", manual, sizeof(manual)); + if (!strcmp(manual, "1")) break; } if (!strcmp(e->slot, "usb")) scan_usb_all(); else if (!strcmp(e->slot, "pcie")) @@ -1630,6 +1773,8 @@ static void handle_client(int fd) } else if (!strcmp(cmd, "disable") && a[0]) { delay_sec = b[0] ? atoi(b) : 0; rc = enqueue_event(EV_DISABLE, a, NULL, delay_sec); + } else if (!strcmp(cmd, "check") && a[0]) { + rc = enqueue_event(EV_CHECK, a, NULL, 0); } else if (!strcmp(cmd, "scan")) { delay_sec = b[0] ? atoi(b) : 0; rc = enqueue_event(EV_SCAN, a[0] ? a : "all", NULL, delay_sec); @@ -1741,6 +1886,9 @@ int main(int argc, char **argv) return 1; } + /* Validate every saved section, even with discovery disabled. */ + check_configured("all"); + threads = calloc((size_t)g.scan_workers, sizeof(*threads)); if (!threads) return 1; diff --git a/application/modem_scan/tests/test_presence.c b/application/modem_scan/tests/test_presence.c new file mode 100644 index 000000000..d495686f9 --- /dev/null +++ b/application/modem_scan/tests/test_presence.c @@ -0,0 +1,113 @@ +#define main qmodem_daemon_main +#include "../src/modem_scand.c" +#undef main +#include + +static void expect_state(const char *expected) +{ + char state[32]; + assert(!uci_get("qmodem.1_1.state", state, sizeof(state))); + assert(!strcmp(state, expected)); +} + +int main(void) +{ + char control[512], original[512], other[512], fail_commit[512]; + const char *sys = getenv("QMODEM_SYSFS"), *fail = getenv("QMODEM_TEST_FAIL"); + struct event *old, *event; + pthread_mutex_init(&g.queue_lock, NULL); + pthread_cond_init(&g.queue_cond, NULL); + g.log_level = LOG_L_ERR; + g.add_retry_max = 2; + g.add_retry_delay = 0; + snprintf(control, sizeof(control), "%s/dev/char/1:3", sys); + snprintf(original, sizeof(original), "%s/devices/usb/1-1/control", sys); + snprintf(other, sizeof(other), "%s/devices/usb/1-10/control", sys); + snprintf(fail_commit, sizeof(fail_commit), "%s.commit", fail); + + assert(enqueue_event(EV_CHECK, "../escape", NULL, 0) == -1); + revoke_ready("1_1"); + assert(!check_modem("1_1")); + assert(!ready_command("check", "1_1")); + expect_state("enabled"); + /* A running scan loses authority immediately on remove arrival. */ + assert(!enqueue_event(EV_ADD, "1-1", "usb", 0)); + old = dequeue_event(); current_event = old; + assert(!enqueue_event(EV_DISABLE, "1-1", NULL, 0)); + assert(ready_command("check", "1_1")); + assert(!event_current()); + assert(!confirm_section("1_1")); + assert(ready_command("check", "1_1")); + finish_event_key(old); free(old); current_event = NULL; + event = dequeue_event(); current_event = event; + process_event(event); + expect_state("disabled"); + finish_event_key(event); free(event); current_event = NULL; + + /* Manual mode validates existing settings and never discovers a new section. */ + assert(!add_modem("1-1", "usb")); + expect_state("enabled"); + assert(!ready_command("check", "1_1")); + assert(!add_modem("2-2", "usb")); + char value[32]; + assert(uci_get("qmodem.2_2", value, sizeof(value))); + assert(!unlink(control)); assert(!symlink(other, control)); + assert(check_modem("1_1")); + expect_state("disabled"); + assert(ready_command("check", "1_1")); + + /* Even if the modem returns, failed host cleanup must not enable it. */ + assert(!unlink(control)); assert(!symlink(original, control)); + int fd = open(fail, O_CREAT | O_WRONLY, 0600); assert(fd >= 0); close(fd); + assert(check_modem("1_1")); + expect_state("disabled"); + assert(ready_command("check", "1_1")); + assert(!enqueue_event(EV_DISABLE, "1-1", NULL, 0)); + event = dequeue_event(); current_event = event; process_event(event); + assert(g.queue_len == 1); /* failed disable queued a bounded retry */ + finish_event_key(event); free(event); current_event = NULL; + unlink(fail); + event = dequeue_event(); current_event = event; process_event(event); + finish_event_key(event); free(event); current_event = NULL; + assert(g.queue_len == 0); + + /* Failed UCI commit revokes proof, even if the set was visible in memory. */ + fd = open(fail_commit, O_CREAT | O_WRONLY, 0600); assert(fd >= 0); close(fd); + assert(confirm_section("1_1")); + assert(ready_command("check", "1_1")); + unlink(fail_commit); + assert(!check_modem("1_1")); + expect_state("enabled"); + + /* Full scans reconcile old sections even with automatic discovery blocked. */ + assert(!unlink(control)); assert(!symlink(other, control)); + assert(!enqueue_event(EV_SCAN, "usb", NULL, 0)); + event = dequeue_event(); current_event = event; process_event(event); + finish_event_key(event); free(event); current_event = NULL; + while (g.queue_len) { + event = dequeue_event(); current_event = event; process_event(event); + finish_event_key(event); free(event); current_event = NULL; + } + expect_state("disabled"); + assert(ready_command("check", "1_1")); + /* Fixed configurations are checked but their user settings are not probed. */ + assert(!unlink(control)); assert(!symlink(original, control)); + assert(!uci_set("qmodem.main.block_auto_probe", "0")); + assert(!uci_set("qmodem.1_1.fixed_device", "1")); + assert(!add_modem("1-1", "usb")); + expect_state("enabled"); + assert(!ready_command("check", "1_1")); + assert(!uci_set("qmodem.1_1.fixed_device", "0")); + assert(!unlink(control)); assert(!symlink(other, control)); + /* Automatic mode can rediscover ports within the same configured slot. */ + assert(!uci_set("qmodem.main.block_auto_probe", "0")); + assert(!enqueue_event(EV_CHECK, "1_1", NULL, 0)); + event = dequeue_event(); current_event = event; process_event(event); + finish_event_key(event); free(event); current_event = NULL; + assert(g.queue_len == 1); + event = dequeue_event(); + assert(event->type == EV_ADD && !strcmp(event->slot, "1-1")); + finish_event_key(event); free(event); + puts("PASS: scanner confirmation, stale events, manual mode, disable retries, commit failure, stale-section reconciliation"); + return 0; +} diff --git a/application/qmodem/files/etc/hotplug.d/net/20-modem-net b/application/qmodem/files/etc/hotplug.d/net/20-modem-net index 6845855d6..fa0f522ec 100755 --- a/application/qmodem/files/etc/hotplug.d/net/20-modem-net +++ b/application/qmodem/files/etc/hotplug.d/net/20-modem-net @@ -1,7 +1,5 @@ #!/bin/sh # Copyright (C) 2024 Tom -manual=$(uci -q get qmodem.main.block_auto_probe) -[ "${manual}" -eq 1 ] && exit logger -t modem_hotplug "net slot: ${DEVPATH} action: ${ACTION}" hotplug_add_delay=$(uci -q get qmodem.main.hotplug_add_delay || echo 8) diff --git a/application/qmodem/files/etc/hotplug.d/usb/20-modem-usb b/application/qmodem/files/etc/hotplug.d/usb/20-modem-usb index cb9cda5cb..1843f106b 100755 --- a/application/qmodem/files/etc/hotplug.d/usb/20-modem-usb +++ b/application/qmodem/files/etc/hotplug.d/usb/20-modem-usb @@ -1,7 +1,5 @@ #!/bin/sh # Copyright (C) 2024 Tom -manual=$(uci -q get qmodem.main.block_auto_probe) -[ "${manual}" -eq 1 ] && exit logger -t modem_hotplug "usb_event slot: ${DEVPATH} action: ${ACTION}" hotplug_add_delay=$(uci -q get qmodem.main.hotplug_add_delay || echo 8) [ -z "${DEVNUM}" ] && exit diff --git a/application/qmodem/files/etc/init.d/qmodem_init b/application/qmodem/files/etc/init.d/qmodem_init index 90429839d..be8ea7bb1 100755 --- a/application/qmodem/files/etc/init.d/qmodem_init +++ b/application/qmodem/files/etc/init.d/qmodem_init @@ -14,7 +14,7 @@ start_service() { config_get enable_usb_scan main enable_usb_scan 0 config_get try_preset_usb main try_preset_usb 1 config_get try_preset_pcie main try_preset_pcie 1 - #once enabled, skip all check or scan + # scand always validates configured devices; this only blocks discovery. [ "$block_auto_probe" -eq 1 ] && return #enabled by default @@ -45,13 +45,11 @@ boot() { _try_vendor_preset_usb() { - config_foreach _try_usb_device modem-device config_foreach _try_usb_slot modem-slot } _try_vendor_preset_pcie() { - config_foreach _try_pcie_device modem-device config_foreach _try_pcie_slot modem-slot } @@ -67,18 +65,6 @@ _try_usb_slot() [ "$type" == "usb" ] && _try_slot $1 } -_try_usb_device() -{ - config_get data_interface $1 data_interface - [ "$data_interface" == "usb" ] && _try_device $1 -} - -_try_pcie_device() -{ - config_get data_interface $1 data_interface - [ "$data_interface" == "pcie" ] && _try_device $1 -} - _scan_usb() { ( sleep 15; /usr/bin/modem_scanc scan usb >/dev/null 2>&1 ) & @@ -127,26 +113,3 @@ _try_slot() ( sleep 1; /usr/share/qmodem/modem_scan.sh disable "$slot" >/dev/null 2>&1 ) & fi } - -_try_device() -{ - config_get path "$1" path - config_get network "$1" network - config_get slot "$1" slot - [ -n "$slot" ] || slot=$(basename "$path") - config_get data_interface "$1" data_interface - if [ ! -d "$path" ]; then - ( sleep 1; /usr/share/qmodem/modem_scan.sh disable "$slot" >/dev/null 2>&1 ) & - return - fi - # if device_path is not parent of netdevice_path, disable device path and scan again - network=$(/usr/share/qmodem/modem_netdev.sh "$1") - netdevice_path=$(readlink -f "/sys/class/net/$network/device/") - device_path=`readlink -f $path` - echo "$netdevice_path" | grep -q "^$device_path" - is_parent=$? - if [ "$is_parent" -ne 0 ] || [ ! -d "$netdevice_path" ]; then - ( sleep 1; /usr/share/qmodem/modem_scan.sh disable "$slot" >/dev/null 2>&1 ) & - ( sleep 1; /usr/share/qmodem/modem_scan.sh add "$(basename "$path")" "$data_interface" >/dev/null 2>&1 ) & - fi -} diff --git a/application/qmodem/files/etc/init.d/qmodem_network b/application/qmodem/files/etc/init.d/qmodem_network index 9e18f938b..d55829ab0 100755 --- a/application/qmodem/files/etc/init.d/qmodem_network +++ b/application/qmodem/files/etc/init.d/qmodem_network @@ -12,6 +12,7 @@ MODEM_RUNDIR="/var/run/qmodem" extra_command "reconfigure" " apply settings while respecting dial enable state" extra_command "dial" " run dial script" +extra_command "offline" " stop and clean host state without modem IO" extra_command "hang" " run hang script" extra_command "redial" " stop previous dial script. run hang script. and run dial script" extra_command "modem_status" " Check dial script status" @@ -60,6 +61,12 @@ hang() _hang $1 } +offline() +{ + qmodem_valid_instance "$1" || return 2 + _hang "$1" offline +} + modem_status() { qmodem_valid_instance "$1" || return 2 @@ -85,24 +92,35 @@ reconfigure() _update_modem() { - local target + local target present=1 target=1 config_get enabled $1 enable_dial config_get state $1 state - if [ "$state" = "disabled" ] || [ "$enabled" = "0" ] || [ "$g_enabled" = "0" ] || [ "$force_stop" = "1" ];then + if [ "$state" != "enabled" ] || [ "$enabled" = "0" ] || [ "$g_enabled" = "0" ] || [ "$force_stop" = "1" ];then target=0 fi + "$SCRIPT_DIR/modem_ready.sh" check "$1" || { target=0; present=0; } data=$(_procd_ubus_call list | jq -r ".$service.instances.modem_$1") [ "$data" == "null" ] && current_status=0 || current_status=1 logger -t qmodem_network "cfg:$1 current:$current_status;target:$target " if [ "$current_status" != "$target" ];then - [ "$target" -eq 1 ] && dial $1 || hang $1 + if [ "$target" -eq 1 ]; then + dial "$1" + elif [ "$present" -eq 1 ]; then + hang "$1" + else + offline "$1" + fi logger -t qmodem_network "update modem cfg: $1 dial:$target" fi } _dial() { + [ "$(uci -q get "qmodem.$1.state")" = enabled ] || return 1 + [ "$(uci -q get "qmodem.$1.enable_dial")" = 1 ] || return 1 + [ "$(uci -q get qmodem.main.enable_dial)" != 0 ] || return 1 + "$SCRIPT_DIR/modem_ready.sh" check "$1" || return 1 procd_open_instance "modem_$1" procd_set_param command "/usr/share/qmodem/modem_dial.sh" "$1" dial procd_set_param respawn 120 15 10 @@ -118,7 +136,17 @@ _hang() rm -f "$MODEM_RUNDIR/${1}_dir/generation" "$MODEM_RUNDIR/${1}_dir/ipv4.json" "$MODEM_RUNDIR/${1}_dir/backend.json" ) procd_kill $service "modem_$1" - /usr/share/qmodem/modem_dial.sh "$1" hang || return $? + /usr/share/qmodem/modem_dial.sh "$1" "${2:-hang}" || return $? + # procd termination is asynchronous; descendants may still own dial.lock. + local attempt=0 + while ! ( exec 7>"$MODEM_RUNDIR/${1}_dir/dial.lock"; flock -n 7 ); do + attempt=$((attempt + 1)) + if [ "$attempt" -ge 15 ]; then + logger -t qmodem_network "Modem $1 stop incomplete: dial process still alive" + return 1 + fi + sleep 1 + done logger -t qmodem_network "Modem $1 Stop Dial and Hang" } diff --git a/application/qmodem/files/usr/share/qmodem/modem_dial.sh b/application/qmodem/files/usr/share/qmodem/modem_dial.sh index 307b6ca8b..f70111870 100755 --- a/application/qmodem/files/usr/share/qmodem/modem_dial.sh +++ b/application/qmodem/files/usr/share/qmodem/modem_dial.sh @@ -527,7 +527,7 @@ update_config() donot_nat=1 fi driver=$(get_driver) - update_sim_slot + if [ "${offline_cleanup:-0}" = 1 ]; then sim_slot=1; else update_sim_slot; fi case $sim_slot in 1) config_get apn $modem_config apn @@ -1029,11 +1029,19 @@ flush_ip_cb() } +dial_device_present() +{ + "$SCRIPT_DIR/modem_ready.sh" check "$modem_config" && return 0 + /usr/bin/modem_scanc check "$modem_config" >/dev/null 2>&1 + return 1 +} + dial(){ exec 7>"$MODEM_RUNDIR/${modem_config}_dir/dial.lock" flock -n 7 || return 1 + "$SCRIPT_DIR/modem_ready.sh" check "$modem_config" || return 1 update_config - [ "$state" != disabled ] && [ "$enable_dial" = 1 ] && [ "$global_dial" != 0 ] || return 1 + [ "$state" = enabled ] && [ "$enable_dial" = 1 ] && [ "$global_dial" != 0 ] || return 1 export QMODEM_INSTANCE="$modem_config" QMODEM_INTERFACE="$interface_name" QMODEM_GENERATION="$$-$(date +%s)" export QMODEM_GENERATION @@ -1051,11 +1059,13 @@ dial(){ m_debug "modem_path=$modem_path,driver=$driver,interface=$interface_name,at_port=$at_port,using_sim_slot:$sim_slot,dns_list:$dns_list,bridge_enabled:$bridge_enabled,bridge_ports:$bridge_ports_selected,bridge_management_ip:$bridge_management_ip" while [ "$dial_prepare" != 1 ] ; do sleep 5 + "$SCRIPT_DIR/modem_ready.sh" check "$modem_config" || return 1 update_config - [ "$state" != disabled ] && [ "$enable_dial" = 1 ] && [ "$global_dial" != 0 ] || return 1 + [ "$state" = enabled ] && [ "$enable_dial" = 1 ] && [ "$global_dial" != 0 ] || return 1 check_dial_prepare dial_prepare=$? done + "$SCRIPT_DIR/modem_ready.sh" check "$modem_config" || return 1 set_if || return 1 export QMODEM_INTERFACE="$interface_name" host_owner=unsupported @@ -1269,6 +1279,7 @@ qmi_dial() fi cmd_line="$cmd_line -f $log_file" while true; do + dial_device_present || return 1 m_debug "dialing: $cmd_line" $cmd_line & local cm_pid=$! @@ -1781,7 +1792,8 @@ at_dial_monitor() if [ $auto_dial_support -eq 0 ]; then m_debug "dialing service is managed by modem(auto dial), do not need monitor" while true; do - sleep 30 + dial_device_present || return 1 + sleep 5 done fi at_dial @@ -1789,6 +1801,7 @@ at_dial_monitor() ipv6_cache=$ipv6 sleep 5 while true; do + dial_device_present || return 1 check_ip case $connection_status in 0) @@ -1825,15 +1838,34 @@ at_dial_monitor() done } +QMODEM_REQUIRE_READY=1 case "$2" in + "offline") + offline_cleanup=1 + debug_subject="modem_offline" + update_config + cleanup_rc=0 + wwan_hang || cleanup_rc=$? + flush_if || cleanup_rc=$? + exit "$cleanup_rc";; "hang") ( mkdir -p "$MODEM_RUNDIR/${modem_config}_dir" exec 9>"$MODEM_RUNDIR/${modem_config}_dir/event.lock"; flock -x 9 rm -f "$MODEM_RUNDIR/${modem_config}_dir/generation" "$MODEM_RUNDIR/${modem_config}_dir/ipv4.json" "$MODEM_RUNDIR/${modem_config}_dir/backend.json" ) debug_subject="modem_hang" + if ! "$SCRIPT_DIR/modem_ready.sh" check "$modem_config"; then + offline_cleanup=1 + fi update_config + if [ "${offline_cleanup:-0}" = 1 ]; then + cleanup_rc=0 + wwan_hang || cleanup_rc=$? + flush_if || cleanup_rc=$? + exit "$cleanup_rc" + fi hang;; "dial") + "$SCRIPT_DIR/modem_ready.sh" check "$modem_config" || exit 1 case "$state" in "disabled") debug_subject="modem_hang" diff --git a/application/qmodem/files/usr/share/qmodem/modem_ready.sh b/application/qmodem/files/usr/share/qmodem/modem_ready.sh new file mode 100755 index 000000000..316d7aed3 --- /dev/null +++ b/application/qmodem/files/usr/share/qmodem/modem_ready.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# Volatile presence proof, never a cross-slot identity or configuration mapping. +script_dir="${QMODEM_HOME:-/usr/share/qmodem}" +sys="${QMODEM_SYSFS:-/sys}" +run="${QMODEM_RUN_DIR:-/var/run/qmodem}" +uci_cmd="${QMODEM_UCI:-uci}" +action="$1" +section="$2" +case "$section" in ''|*[!A-Za-z0-9_]*) exit 2;; esac +get() { "$uci_cmd" -q get "qmodem.$1"; } + +snapshot() { + [ "$(get "$section")" = modem-device ] || return 1 + configured=$(get "$section.path") || return 1 + root=$(readlink -f "$configured") || return 1 + case "$root" in "$sys"/devices/*) ;; *) return 1;; esac + [ -d "$root" ] || return 1 + port=$(get "$section.at_port") || return 1 + [ -c "$port" ] || return 1 + numbers=$(stat -L -c '%t:%T' "$port") || return 1 + devnum=$(printf '%d:%d' "0x${numbers%:*}" "0x${numbers#*:}") || return 1 + control=$(readlink -f "$sys/dev/char/$devnum") || return 1 + [ -d "$control" ] || return 1 + control_root="$root" + case "$control" in + "$root"/*) ;; + *) + # A PCIe data slot may explicitly own a separate USB control slot. + [ "$(get "$section.data_interface")" = pcie ] || return 1 + slot=${configured%/}; slot=${slot##*/} + associated="" + for cfg in $("$uci_cmd" -q show qmodem | sed -n 's/^qmodem\.\([A-Za-z0-9_]*\)=modem-slot$/\1/p'); do + [ "$(get "$cfg.type")" = pcie ] || continue + [ "$(get "$cfg.slot")" = "$slot" ] || continue + usb=$(get "$cfg.associated_usb") || continue + [ -z "$associated" ] || [ "$associated" = "$usb" ] || return 1 + associated="$usb" + done + case "$associated" in ''|*[!A-Za-z0-9.-]*) return 1;; esac + control_root=$(readlink -f "$sys/bus/usb/devices/$associated") || return 1 + [ -d "$control_root" ] || return 1 + case "$control" in "$control_root"/*) ;; *) return 1;; esac + ;; + esac + net=$("$script_dir/modem_netdev.sh" "$section") || return 1 + netpath=$(readlink -f "$sys/class/net/$net") || return 1 + [ -d "$netpath" ] || return 1 + case "$netpath" in "$root"/*) ;; *) return 1;; esac + # Inodes distinguish a new enumeration at the same path in this boot. + printf '%s\n' "$configured" "$port" "$net" "$root" "$control" "$netpath" + stat -L -c '%i' "$root" "$control_root" "$control" "$netpath" +} + +case "$action" in + snapshot) snapshot;; + check) + [ -f "$run/${section}_dir/ready" ] || exit 1 + current=$(snapshot) || exit 1 + [ "$current" = "$(cat "$run/${section}_dir/ready")" ] + ;; + confirm) + current=$(snapshot) || exit 1 + mkdir -p "$run/${section}_dir" || exit 1 + tmp="$run/${section}_dir/ready.$$" + trap 'rm -f "$tmp"' EXIT HUP INT TERM + printf '%s\n' "$current" > "$tmp" && mv "$tmp" "$run/${section}_dir/ready" + ;; + *) exit 2;; +esac diff --git a/application/qmodem/files/usr/share/qmodem/modem_util.sh b/application/qmodem/files/usr/share/qmodem/modem_util.sh index b85798640..040325f2e 100755 --- a/application/qmodem/files/usr/share/qmodem/modem_util.sh +++ b/application/qmodem/files/usr/share/qmodem/modem_util.sh @@ -97,6 +97,10 @@ at() [ -n "$port_key" ] || exit 2 mkdir -p /var/lock exec 6>"/var/lock/qmodem-at-$port_key.lock"; flock -x 6 || exit 1 + if [ "${QMODEM_REQUIRE_READY:-0}" = 1 ]; then + "${SCRIPT_DIR}/modem_ready.sh" check "$modem_config" || exit 1 + [ "$1" = "$(uci -q get "qmodem.$modem_config.at_port")" ] || exit 1 + fi local at_port=$1 local new_str="${2/[$]/$}" local atcmd="${new_str/\"/\"}" diff --git a/application/qmodem/tests/test_modem_presence.sh b/application/qmodem/tests/test_modem_presence.sh new file mode 100755 index 000000000..4e1cedc47 --- /dev/null +++ b/application/qmodem/tests/test_modem_presence.sh @@ -0,0 +1,177 @@ +#!/bin/sh +# Real presence helper + scanner state transitions, with isolated UCI/sysfs. +set -eu +repo=$(CDPATH= cd -- "$(dirname "$0")/../../.." && pwd) +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' EXIT HUP INT TERM +export QMODEM_HOME="$repo/application/qmodem/files/usr/share/qmodem" +export QMODEM_SYSFS="$fixture/sys" QMODEM_RUN_DIR="$fixture/run" +export QMODEM_TEST_DB="$fixture/config.json" QMODEM_TEST_CALLS="$fixture/calls" +export QMODEM_TEST_FAIL="$fixture/fail" +export QMODEM_UCI="$fixture/bin/uci" +export PATH="$fixture/bin:$PATH" +mkdir -p "$fixture/bin" "$QMODEM_RUN_DIR" "$QMODEM_SYSFS/dev/char" "$QMODEM_SYSFS/class/net" \ + "$QMODEM_SYSFS/devices/usb/1-1/control" "$QMODEM_SYSFS/devices/usb/1-1/data/net/wwan0" \ + "$QMODEM_SYSFS/devices/usb/1-10/control" "$QMODEM_SYSFS/bus/usb/devices" +ln -s "$QMODEM_SYSFS/devices/usb/1-1" "$QMODEM_SYSFS/bus/usb/devices/1-1" +ln -s "$QMODEM_SYSFS/devices/usb/1-1/control" "$QMODEM_SYSFS/dev/char/1:3" +ln -s "$QMODEM_SYSFS/devices/usb/1-1/data/net/wwan0" "$QMODEM_SYSFS/class/net/wwan0" +cat > "$fixture/bin/uci" <<'PY' +#!/usr/bin/env python3 +import json, os, sys +p = os.environ['QMODEM_TEST_DB'] +d = json.load(open(p)) +a = [x for x in sys.argv[1:] if x != '-q'] +if a[0] == 'get': + if a[1] not in d: sys.exit(1) + print(d[a[1]]) +elif a[0] == 'show': + for k, v in d.items(): + if k.startswith(a[1] + '.'): + print(k + '=' + (v if k.count('.') == 1 else "'" + v + "'")) +elif a[0] == 'set': + k, v = a[1].split('=', 1) + d[k] = v + json.dump(d, open(p, 'w')) +elif a[0] == 'commit': + if os.path.exists(os.environ['QMODEM_TEST_FAIL'] + '.commit'): sys.exit(1) +else: + sys.exit(2) +PY +cat > "$fixture/bin/network-init" <<'SH' +#!/bin/sh +printf '%s\n' "$*" >> "$QMODEM_TEST_CALLS" +[ "$1" != offline ] || [ ! -e "$QMODEM_TEST_FAIL" ] +SH +chmod +x "$fixture/bin/uci" "$fixture/bin/network-init" +ln -s "$QMODEM_HOME/modem_ready.sh" "$fixture/bin/modem-ready" +python3 - <<'PY' +import json, os +s = os.environ['QMODEM_SYSFS'] +json.dump({'qmodem.main.block_auto_probe':'1', 'qmodem.main.enable_dial':'1', + 'qmodem.1_1':'modem-device', 'qmodem.1_1.path':s+'/bus/usb/devices/1-1/', + 'qmodem.1_1.at_port':'/dev/null', 'qmodem.1_1.data_interface':'usb', + 'qmodem.1_1.state':'enabled', 'qmodem.1_1.enable_dial':'1', 'qmodem.1_1.apn':'keep-me'}, + open(os.environ['QMODEM_TEST_DB'], 'w')) +PY +ready="$QMODEM_HOME/modem_ready.sh" +# A saved enabled flag is insufficient before confirmation. +! "$ready" check 1_1 +"$ready" confirm 1_1 +"$ready" check 1_1 +# Prefix siblings are not children: 1-10 must never be mistaken for 1-1. +rm "$QMODEM_SYSFS/dev/char/1:3" +ln -s "$QMODEM_SYSFS/devices/usb/1-10/control" "$QMODEM_SYSFS/dev/char/1:3" +! "$ready" check 1_1 +! "$ready" confirm 1_1 +rm "$QMODEM_SYSFS/dev/char/1:3" +ln -s "$QMODEM_SYSFS/devices/usb/1-1/control" "$QMODEM_SYSFS/dev/char/1:3" +# A new enumeration at the exact same path invalidates the old proof. +mv "$QMODEM_SYSFS/devices/usb/1-1/control" "$QMODEM_SYSFS/devices/usb/1-1/old-control" +mkdir "$QMODEM_SYSFS/devices/usb/1-1/control" +! "$ready" check 1_1 +"$ready" confirm 1_1 +# Nested QMI data endpoint remains supported. +mkdir -p "$QMODEM_SYSFS/devices/usb/1-1/data/net/wwan0/net/wwan0_1" +ln -s "$QMODEM_SYSFS/devices/usb/1-1/data/net/wwan0/net/wwan0_1" "$QMODEM_SYSFS/class/net/wwan0_1" +! "$ready" check 1_1 +"$ready" confirm 1_1 +"$ready" check 1_1 +rmdir "$QMODEM_SYSFS/devices/usb/1-1/data/net/wwan0/net/wwan0_1" "$QMODEM_SYSFS/devices/usb/1-1/data/net/wwan0/net" +rm "$QMODEM_SYSFS/class/net/wwan0_1" +# PCIe association is explicit and is not inferred from serial/VID/PID. +mkdir -p "$QMODEM_SYSFS/devices/pci/0000:01:00.0/data/net/wwan1" +ln -s "$QMODEM_SYSFS/devices/pci/0000:01:00.0/data/net/wwan1" "$QMODEM_SYSFS/class/net/wwan1" +uci set qmodem.pcie=modem-device +uci set "qmodem.pcie.path=$QMODEM_SYSFS/devices/pci/0000:01:00.0" +uci set qmodem.pcie.at_port=/dev/null +uci set qmodem.pcie.data_interface=pcie +! "$ready" confirm pcie +uci set qmodem.preset=modem-slot +uci set qmodem.preset.type=pcie +uci set qmodem.preset.slot=0000:01:00.0 +uci set qmodem.preset.associated_usb=1-1 +"$ready" confirm pcie +"$ready" check pcie +uci set qmodem.preset.associated_usb=2-1 +# Exercise the real procd admission function. Manual dial shares this gate. +eval "$(awk '$0 == "_dial()" { capture=1 } capture { print } capture && $0 == "}" { exit }' "$repo/application/qmodem/files/etc/init.d/qmodem_network")" +SCRIPT_DIR="$QMODEM_HOME" +procd_open_instance() { printf '%s\n' "$1" >> "$fixture/procd"; } +procd_set_param() { :; } +procd_close_instance() { :; } +logger() { :; } +rm -f "$QMODEM_RUN_DIR/1_1_dir/ready" +! _dial 1_1 +[ ! -e "$fixture/procd" ] +"$ready" confirm 1_1 +_dial 1_1 +[ "$(cat "$fixture/procd")" = modem_1_1 ] +uci set qmodem.1_1.state=disabled +! _dial 1_1 +[ "$(wc -l < "$fixture/procd")" -eq 1 ] +uci set qmodem.1_1.state=enabled +# Execute the actual AT boundary and offline dispatch with transports stubbed. +# Only redirect its lock directory into this fixture. +awk '$0 == "at()" { capture=1 } capture { print } capture && $0 == ")" { exit }' \ + "$QMODEM_HOME/modem_util.sh" | sed "s|/var/lock|$fixture/locks|g" > "$fixture/at-function" +cat > "$fixture/test-at" <<'SH' +#!/bin/bash +set -e +source "$1/at-function" +SCRIPT_DIR="$QMODEM_HOME" +modem_config=1_1 +QMODEM_REQUIRE_READY=1 +qmodem_testcase_collect_enabled() { return 1; } +# Use a distinct name to avoid transport argument shadowing. +transport_calls="$1/transport" +tom_modem() { echo transport >> "$transport_calls"; } +"$SCRIPT_DIR/modem_ready.sh" confirm 1_1 +at /dev/null ATI +[ "$(wc -l < "$transport_calls")" -eq 1 ] +rm "$QMODEM_RUN_DIR/1_1_dir/ready" +! at /dev/null ATI +[ "$(wc -l < "$transport_calls")" -eq 1 ] +"$SCRIPT_DIR/modem_ready.sh" confirm 1_1 +! at /dev/zero ATI +[ "$(wc -l < "$transport_calls")" -eq 1 ] +SH +bash "$fixture/test-at" "$fixture" +awk '$0 == "update_config()" { capture=1 } capture { print } capture && $0 == "}" { exit }' \ + "$QMODEM_HOME/modem_dial.sh" > "$fixture/update-config" +awk '$0 == "QMODEM_REQUIRE_READY=1" { capture=1 } capture { print }' \ + "$QMODEM_HOME/modem_dial.sh" > "$fixture/dispatch" +cat > "$fixture/test-offline" <<'SH' +#!/bin/bash +source "$1/update-config" +SCRIPT_DIR="$QMODEM_HOME" +modem_config=1_1 +config_load() { :; } +config_foreach() { :; } +config_get() { local value="${4:-}"; printf -v "$1" '%s' "$value"; } +get_platform_suggest_pdp_index() { echo 1; } +get_driver() { echo ncm; } +update_sim_slot() { echo unexpected-at >> "$QMODEM_TEST_CALLS"; } +wwan_hang() { echo stop-child >> "$QMODEM_TEST_CALLS"; return 1; } +flush_if() { echo flush-host >> "$QMODEM_TEST_CALLS"; } +set -- "$1" offline +source "$1/dispatch" +SH +if bash "$fixture/test-offline" "$fixture"; then + echo 'offline cleanup must report a failed child stop' >&2 + exit 1 +fi +! grep -q '^unexpected-at$' "$QMODEM_TEST_CALLS" +grep -q '^stop-child$' "$QMODEM_TEST_CALLS" +grep -q '^flush-host$' "$QMODEM_TEST_CALLS" +# Exercise the actual scanner lifecycle, not a copy of its state machine. +${CC:-cc} ${CFLAGS:-} -D_GNU_SOURCE -std=c99 -pthread \ + "-DQMODEM_RUN_DIR=\"$QMODEM_RUN_DIR\"" \ + "-DQMODEM_CONFIG_LOCK=\"$fixture/config.lock\"" \ + '-DQMODEM_READY_HELPER="modem-ready"' '-DQMODEM_NETWORK_INIT="network-init"' \ + "$repo/application/modem_scan/tests/test_presence.c" ${LDFLAGS:--ljson-c} -o "$fixture/test-presence" +"$fixture/test-presence" +[ "$(uci -q get qmodem.1_1.apn)" = keep-me ] +grep -q '^offline 1_1$' "$QMODEM_TEST_CALLS" +grep -q '^reload$' "$QMODEM_TEST_CALLS" +echo 'PASS: boot gate, tty reuse, same-slot re-enumeration, nested QMI, explicit PCIe association and scanner lifecycle' diff --git a/docs/Dev_guide_modem_scan_workflow.md b/docs/Dev_guide_modem_scan_workflow.md index c3ad0d830..baeb62818 100644 --- a/docs/Dev_guide_modem_scan_workflow.md +++ b/docs/Dev_guide_modem_scan_workflow.md @@ -1,5 +1,38 @@ # 增加模组支持文档 +## 扫描与拨号的生命周期 + +`modem_scand` 启动时检查全部已有的 `modem-device`,全量扫描也会检查已有配置。 +配置仍按 slot 独立保存;不同 slot 不按序列号、VID/PID 合并,也不迁移 APN 等用户配置。 + +拨号需要 `/var/run/qmodem/
_dir/ready` 中的本次运行确认结果,不能仅凭持久化的 +`state=enabled` 启动。确认过程检查设备路径、AT 字符设备的 sysfs 归属和数据网卡, +并记录 sysfs inode,以发现同一路径的重新枚举。PCIe 使用独立 USB 控制口时,仅接受 +已有 `modem-slot.associated_usb` 的显式关联;嵌套 QMI 数据网卡沿用 `modem_netdev.sh`。 +该文件只用于运行期就绪检查,不是模组身份或跨 slot 配置映射。 + +自动发现成功后,守护进程先提交扫描配置,再确认就绪并触发拨号服务 reload。 +`qmodem_network` 创建 procd 实例前检查就绪状态,拨号脚本在读取配置、启动拨号以及 +发送 AT 命令前再次检查。未知状态默认不启动。不要通过手工创建 ready 文件绕过检查。 + +收到 remove/disable 后立即撤销确认结果,使正在执行的旧扫描不能再次发布就绪状态。 +随后设置 `state=disabled`,通过 `qmodem_network offline
` 停止 procd 实例和 +所属连接管理进程,清理该段拥有的网络接口。offline 不查询 SIM、不向旧 AT 口发送 +挂断命令。停拨失败保持未就绪,按扫描重试参数进行有限次数的重试。 + +`block_auto_probe=1` 只禁止自动发现和探测改写;已有配置的存在性检查和掉线处理仍然执行。 +`fixed_device=1` 同样需要存在性确认。手动配置的路径或端口已失效时保持停用,需要用户 +修正配置后重查;不会自动将它绑定到其他 slot。自动模式可以重新探测同一 slot 的端口。 + +可用 `modem_scanc check
` 请求重新检查,或用 `modem_scanc scan all` 检查全部 +旧段并按当前策略发现设备。这些命令返回的是入队结果,不代表扫描或停拨已经完成。 +设备验收还应检查 UCI 状态、procd 实例、拨号子进程和该段的网络接口。 + +宿主机回归入口:`sh application/qmodem/tests/test_modem_presence.sh`,需要 C 编译器、 +json-c 开发库、Python 3 和 Bash。该测试使用隔离的 sysfs/UCI/procd fixture,覆盖启动门禁、 +端口复用、同路径重新枚举、PCIe 关联、旧事件失效、提交失败和停拨重试;不替代实机拔插验证。 +本改动需要同时更新 `qmodem` 和 `modem_scan` 包。 + ## 流程简介 在以下几个地方会触发模组扫描 @@ -18,4 +51,4 @@ ### 尝试添加设备流程: 设备经过扫描流程后,会将 devpath 和 slot_type 传入添加流程 设备添加时会向设备发送 ```ATI``` 命令,如果包含返回 ```OK``` 字符串的则认为该端口可用,标记为可用端口后加入列表 -检查完所有端口后 \ No newline at end of file +检查完所有端口后