mirror of
https://github.com/apache/nuttx-apps.git
synced 2026-09-12 13:40:16 +00:00
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:
parent
a90c12b6da
commit
bc0ed23a5d
6 changed files with 201 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue