system/nxinit: add a per-service "console" option

Services started by nxinit (e.g. "sh") do not open a console device on
their own, unlike nsh_main, which explicitly does so via
nsh_consolemain()/nsh_waitusbready() for USB gadget consoles
(CDC-ACM/PL2303). When such a board switches its top-level init from
nsh_main to nxinit, no code path ever registers/connects the USB
console gadget, and a service that just execs a plain "sh" inherits
whatever (invalid, for a USB gadget console not yet opened at the time
the idle task file descriptors are set up) stdio nxinit itself has.

Add a "console [<device>]" service option: a service declared with it
gets the given device (CONFIG_SYSTEM_NXINIT_CONSOLE_DEV, "/dev/console"
by default, if no device is given) opened and dup'd onto its stdin,
stdout and stderr via posix_spawn_file_actions before it is spawned.
This does not depend on nsh being enabled at all.

For a USB gadget console, the device does not exist until the gadget
is actually registered; boards using one are expected to bring it up
themselves before any service using "console" is started (e.g. via an
"exec -- sercon" action in their init.rc, since apps/system/cdcacm
already implements exactly that registration step and does not depend
on nxinit or nsh either).

console_file_actions() runs after the previous commit's time_started
update, so a failure to build the console's file actions is covered by
the same up to date timestamp - no separate clock_gettime() call is
needed on this failure path.

Covered by a new unit test, test_nxinit_service_console_option, that
exercises the option with and without an explicit device, and confirms
services without the option are left untouched.

Assisted-by: Kiro:claude-sonnet-5
Signed-off-by: wangjianyu3 <wangjianyu3@xiaomi.com>
This commit is contained in:
wangjianyu3 2026-09-08 13:20:59 +08:00 committed by Xiang Xiao
parent a90c12b6da
commit bc0ed23a5d
6 changed files with 201 additions and 2 deletions

View file

@ -111,6 +111,20 @@ config SYSTEM_NXINIT_SERVICE_RESTART_PERIOD
int "Service restart period in ms"
default 5000
config SYSTEM_NXINIT_CONSOLE_DEV
string "Default console device of a service"
default "/dev/console"
---help---
The device used as stdin/stdout/stderr of a service declared with the
option "console" and no explicit device, i.e. "console" rather than
"console <device>".
This is needed by services which expect a controlling terminal but do
not open one themselves (e.g. plain "sh"), and is the only way to get
a console when the console device is not available at the time the
file descriptors of the idle task are set up, as is the case for a USB
gadget console.
comment "NXInit Testing"
config SYSTEM_NXINIT_TEST

View file

@ -29,12 +29,14 @@
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <spawn.h>
#include <sys/param.h>
#include <unistd.h>
#include "init.h"
#include "parser.h"
@ -100,6 +102,8 @@ static int option_override(FAR struct service_manager_s *sm,
int argc, FAR char **argv);
static int option_oneshot(FAR struct service_manager_s *sm,
int argc, FAR char **argv);
static int option_console(FAR struct service_manager_s *sm,
int argc, FAR char **argv);
#ifdef CONFIG_BOARDCTL_RESET
static int option_reboot_on_failure(FAR struct service_manager_s *sm,
int argc, FAR char **argv);
@ -116,6 +120,7 @@ static const struct cmd_map_s g_option[] =
{"restart_period", 2, 2, option_restart_period},
{"override", 1, 1, option_override},
{"oneshot", 1, 1, option_oneshot},
{"console", 1, 2, option_console},
#ifdef CONFIG_BOARDCTL_RESET
{"reboot_on_failure", 2, 2, option_reboot_on_failure},
#endif
@ -128,6 +133,7 @@ static const struct flag_str_s g_flag_str[] =
{SVC_ONESHOT, "oneshot"},
{SVC_RUNNING, "running"},
{SVC_RESTARTING, "restarting"},
{SVC_CONSOLE, "console"},
{SVC_GENTLE_KILL, "gentle_kill"},
{SVC_REMOVE, "remove"},
{SVC_SIGKILL, "sigkill"},
@ -197,6 +203,7 @@ static void remove_service(FAR struct service_s *service)
free(service->argv[i]);
}
free(service->console);
list_delete(&service->node);
free(service);
}
@ -273,6 +280,43 @@ static int option_oneshot(FAR struct service_manager_s *sm,
return 0;
}
/****************************************************************************
* Name: option_console
*
* Description:
* Handle the service option "console [<device>]". The service is given
* 'device' (CONFIG_SYSTEM_NXINIT_CONSOLE_DEV if omitted) as its stdin,
* stdout and stderr, so that a service which does not open a console
* device on its own still gets a working console.
*
****************************************************************************/
static int option_console(FAR struct service_manager_s *sm,
int argc, FAR char **argv)
{
FAR struct service_s *s = list_last_entry(&sm->services, struct service_s,
node);
add_flags(s, SVC_CONSOLE);
if (argc > 1)
{
/* 'argv' points into the parser line buffer, which is reused for the
* next line, so the device name must be duplicated here.
*/
free(s->console);
s->console = strdup(argv[1]);
if (s->console == NULL)
{
init_err("Alloc console device");
return -ENOMEM;
}
}
return 0;
}
#ifdef CONFIG_BOARDCTL_RESET
static int option_reboot_on_failure(FAR struct service_manager_s *sm,
int argc, FAR char **argv)
@ -430,8 +474,59 @@ void init_service_reap(FAR struct service_s *service, int status)
}
}
/****************************************************************************
* Name: console_file_actions
*
* Description:
* Build the spawn file actions which redirect the stdio of a service
* flagged SVC_CONSOLE to its console device. The actions are performed
* in the context of the new task, so the stdio of NxInit itself is left
* untouched.
*
****************************************************************************/
static int console_file_actions(FAR posix_spawn_file_actions_t *actions,
FAR struct service_s *service)
{
FAR const char *dev = service->console ?
service->console : CONFIG_SYSTEM_NXINIT_CONSOLE_DEV;
int ret;
ret = posix_spawn_file_actions_init(actions);
if (ret != 0)
{
init_err("posix_spawn_file_actions_init %d", ret);
return -ret;
}
ret = posix_spawn_file_actions_addopen(actions, STDIN_FILENO, dev,
O_RDWR, 0);
if (ret == 0)
{
ret = posix_spawn_file_actions_adddup2(actions, STDIN_FILENO,
STDOUT_FILENO);
}
if (ret == 0)
{
ret = posix_spawn_file_actions_adddup2(actions, STDIN_FILENO,
STDERR_FILENO);
}
if (ret != 0)
{
init_err("Add console '%s' file action %d", dev, ret);
posix_spawn_file_actions_destroy(actions);
return -ret;
}
return 0;
}
int init_service_start(FAR struct service_s *service)
{
FAR posix_spawn_file_actions_t *pactions = NULL;
posix_spawn_file_actions_t actions;
posix_spawnattr_t attr;
sigset_t mask;
int ret;
@ -472,9 +567,27 @@ int init_service_start(FAR struct service_s *service)
clock_gettime(CLOCK_MONOTONIC, &service->time_started);
ret = posix_spawnp(&pid, service->argv[2], NULL, &attr, &service->argv[2],
environ);
if (check_flags(service, SVC_CONSOLE))
{
ret = console_file_actions(&actions, service);
if (ret < 0)
{
posix_spawnattr_destroy(&attr);
init_service_reap(service, -ret);
return ret;
}
pactions = &actions;
}
ret = posix_spawnp(&pid, service->argv[2], pactions, &attr,
&service->argv[2], environ);
posix_spawnattr_destroy(&attr);
if (pactions != NULL)
{
posix_spawn_file_actions_destroy(pactions);
}
if (ret != 0)
{
init_err("Starting service '%s': %d", service->argv[1], ret);

View file

@ -44,6 +44,7 @@
#define SVC_ONESHOT (1 << 1) /* do not restart on exit */
#define SVC_RUNNING (1 << 2) /* currently active */
#define SVC_RESTARTING (1 << 3) /* waiting to restart */
#define SVC_CONSOLE (1 << 4) /* requires a console as its stdio */
/* This service should be stopped with SIGTERM instead of SIGKILL.
* Will still be SIGKILLed after timeout period of 200 ms.
@ -105,6 +106,12 @@ struct service_s
int restart_period;
pid_t pid;
/* The device given by the service option "console". NULL means that the
* default console device is used. Only meaningful with SVC_CONSOLE.
*/
FAR char *console;
/* The "target" of service option "reboot_on_failure" */
#ifdef CONFIG_BOARDCTL_RESET

View file

@ -58,6 +58,7 @@ int main(int argc, FAR char *argv[])
cmocka_unit_test(test_nxinit_service_duplicate_conflict),
cmocka_unit_test(test_nxinit_service_override_replaces_duplicate),
cmocka_unit_test(test_nxinit_service_args_max_boundary),
cmocka_unit_test(test_nxinit_service_console_option),
};
return cmocka_run_group_tests(nxinit_tests, test_nxinit_group_setup,

View file

@ -77,5 +77,6 @@ void test_nxinit_action_event_and_semantics(FAR void **state);
void test_nxinit_service_duplicate_conflict(FAR void **state);
void test_nxinit_service_override_replaces_duplicate(FAR void **state);
void test_nxinit_service_args_max_boundary(FAR void **state);
void test_nxinit_service_console_option(FAR void **state);
#endif /* __APPS_SYSTEM_NXINIT_TEST_TEST_NXINIT_H */

View file

@ -80,6 +80,7 @@ static void service_manager_free_all(FAR struct service_manager_s *sm)
free(s->argv[i]);
}
free(s->console);
list_delete(&s->node);
free(s);
}
@ -249,3 +250,65 @@ void test_nxinit_service_args_max_boundary(FAR void **state)
#undef NARGS_AT_LIMIT
}
/****************************************************************************
* Name: test_nxinit_service_console_option
*
* Description:
* The "console" option flags the service with SVC_CONSOLE. Without an
* argument the default console device is used (console == NULL); with an
* argument the device name is duplicated into the service, since the
* parser reuses its line buffer for the next line.
****************************************************************************/
void test_nxinit_service_console_option(FAR void **state)
{
struct service_manager_s sm;
struct parser_s parser =
{
"service", init_service_parse, init_service_check, &sm
};
char decl1[] = "service console1 /bin/sh";
char opt_default[] = " console";
char decl2[] = "service console2 /bin/sh";
char opt_device[] = " console /dev/ttyACM0";
char decl3[] = "service plain /bin/sh";
FAR struct service_s *s;
service_manager_init(&sm);
/* "console" without an argument: flagged, default device. */
assert_int_equal(init_service_parse(&parser, true, decl1), 0);
assert_int_equal(init_service_parse(&parser, false, opt_default), 0);
s = list_last_entry(&sm.services, struct service_s, node);
assert_int_equal(s->flags & SVC_CONSOLE, SVC_CONSOLE);
assert_null(s->console);
/* "console <device>": flagged, device duplicated (not aliased into the
* caller's line buffer, which is reused for the next line).
*/
assert_int_equal(init_service_parse(&parser, true, decl2), 0);
assert_int_equal(init_service_parse(&parser, false, opt_device), 0);
s = list_last_entry(&sm.services, struct service_s, node);
assert_int_equal(s->flags & SVC_CONSOLE, SVC_CONSOLE);
assert_non_null(s->console);
assert_string_equal(s->console, "/dev/ttyACM0");
assert_ptr_not_equal(s->console, opt_device + 10);
/* A service without the option keeps its stdio untouched. */
assert_int_equal(init_service_parse(&parser, true, decl3), 0);
s = list_last_entry(&sm.services, struct service_s, node);
assert_int_equal(s->flags & SVC_CONSOLE, 0);
assert_null(s->console);
assert_int_equal(init_service_check(&parser), 0);
service_manager_free_all(&sm);
}