core: add support for naming file descriptors passed using socket activation
This adds support for naming file descriptors passed using socket activation. The names are passed in a new $LISTEN_FDNAMES= environment variable, that matches the existign $LISTEN_FDS= one and contains a colon-separated list of names. This also adds support for naming fds submitted to the per-service fd store using FDNAME= in the sd_notify() message. This also adds a new FileDescriptorName= setting for socket unit files to set the name for fds created by socket units. This also adds a new call sd_listen_fds_with_names(), that is similar to sd_listen_fds(), but also returns the names of the fds. systemd-activate gained the new --fdname= switch to specify a name for testing socket activation. This is based on #1247 by Maciej Wereski. Fixes #1247.
This commit is contained in:
+45
-16
@@ -26,7 +26,7 @@
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "systemd/sd-daemon.h"
|
||||
#include "sd-daemon.h"
|
||||
|
||||
#include "log.h"
|
||||
#include "macro.h"
|
||||
@@ -38,6 +38,7 @@ static char** arg_listen = NULL;
|
||||
static bool arg_accept = false;
|
||||
static char** arg_args = NULL;
|
||||
static char** arg_setenv = NULL;
|
||||
static const char *arg_fdname = NULL;
|
||||
|
||||
static int add_epoll(int epoll_fd, int fd) {
|
||||
struct epoll_event ev = {
|
||||
@@ -136,8 +137,8 @@ static int launch(char* name, char **argv, char **env, int fds) {
|
||||
|
||||
length = strv_length(arg_setenv);
|
||||
|
||||
/* PATH, TERM, HOME, USER, LISTEN_FDS, LISTEN_PID, NULL */
|
||||
envp = new0(char *, length + 7);
|
||||
/* PATH, TERM, HOME, USER, LISTEN_FDS, LISTEN_PID, LISTEN_FDNAMES, NULL */
|
||||
envp = new0(char *, length + 8);
|
||||
if (!envp)
|
||||
return log_oom();
|
||||
|
||||
@@ -145,7 +146,9 @@ static int launch(char* name, char **argv, char **env, int fds) {
|
||||
if (strchr(*s, '='))
|
||||
envp[n_env++] = *s;
|
||||
else {
|
||||
_cleanup_free_ char *p = strappend(*s, "=");
|
||||
_cleanup_free_ char *p;
|
||||
|
||||
p = strappend(*s, "=");
|
||||
if (!p)
|
||||
return log_oom();
|
||||
envp[n_env] = strv_find_prefix(env, p);
|
||||
@@ -164,15 +167,37 @@ static int launch(char* name, char **argv, char **env, int fds) {
|
||||
(asprintf((char**)(envp + n_env++), "LISTEN_PID=%d", getpid()) < 0))
|
||||
return log_oom();
|
||||
|
||||
if (arg_fdname) {
|
||||
char *e;
|
||||
|
||||
e = strappend("LISTEN_FDNAMES=", arg_fdname);
|
||||
if (!e)
|
||||
return log_oom();
|
||||
|
||||
for (i = 1; i < (unsigned) fds; i++) {
|
||||
char *c;
|
||||
|
||||
c = strjoin(e, ":", arg_fdname, NULL);
|
||||
if (!c) {
|
||||
free(e);
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
free(e);
|
||||
e = c;
|
||||
}
|
||||
|
||||
envp[n_env++] = e;
|
||||
}
|
||||
|
||||
tmp = strv_join(argv, " ");
|
||||
if (!tmp)
|
||||
return log_oom();
|
||||
|
||||
log_info("Execing %s (%s)", name, tmp);
|
||||
execvpe(name, argv, envp);
|
||||
log_error_errno(errno, "Failed to execp %s (%s): %m", name, tmp);
|
||||
|
||||
return -errno;
|
||||
return log_error_errno(errno, "Failed to execp %s (%s): %m", name, tmp);
|
||||
}
|
||||
|
||||
static int launch1(const char* child, char** argv, char **env, int fd) {
|
||||
@@ -289,6 +314,7 @@ static void help(void) {
|
||||
static int parse_argv(int argc, char *argv[]) {
|
||||
enum {
|
||||
ARG_VERSION = 0x100,
|
||||
ARG_FDNAME,
|
||||
};
|
||||
|
||||
static const struct option options[] = {
|
||||
@@ -297,11 +323,12 @@ static int parse_argv(int argc, char *argv[]) {
|
||||
{ "listen", required_argument, NULL, 'l' },
|
||||
{ "accept", no_argument, NULL, 'a' },
|
||||
{ "setenv", required_argument, NULL, 'E' },
|
||||
{ "environment", required_argument, NULL, 'E' }, /* alias */
|
||||
{ "environment", required_argument, NULL, 'E' }, /* legacy alias */
|
||||
{ "fdname", required_argument, NULL, ARG_FDNAME },
|
||||
{}
|
||||
};
|
||||
|
||||
int c;
|
||||
int c, r;
|
||||
|
||||
assert(argc >= 0);
|
||||
assert(argv);
|
||||
@@ -315,25 +342,27 @@ static int parse_argv(int argc, char *argv[]) {
|
||||
case ARG_VERSION:
|
||||
return version();
|
||||
|
||||
case 'l': {
|
||||
int r = strv_extend(&arg_listen, optarg);
|
||||
case 'l':
|
||||
r = strv_extend(&arg_listen, optarg);
|
||||
if (r < 0)
|
||||
return r;
|
||||
return log_oom();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'a':
|
||||
arg_accept = true;
|
||||
break;
|
||||
|
||||
case 'E': {
|
||||
int r = strv_extend(&arg_setenv, optarg);
|
||||
case 'E':
|
||||
r = strv_extend(&arg_setenv, optarg);
|
||||
if (r < 0)
|
||||
return r;
|
||||
return log_oom();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ARG_FDNAME:
|
||||
arg_fdname = optarg;
|
||||
break;
|
||||
|
||||
case '?':
|
||||
return -EINVAL;
|
||||
|
||||
+44
-3
@@ -277,8 +277,8 @@ char **strv_split_newlines(const char *s) {
|
||||
}
|
||||
|
||||
int strv_split_extract(char ***t, const char *s, const char *separators, ExtractFlags flags) {
|
||||
size_t n = 0, allocated = 0;
|
||||
_cleanup_strv_free_ char **l = NULL;
|
||||
size_t n = 0, allocated = 0;
|
||||
int r;
|
||||
|
||||
assert(t);
|
||||
@@ -302,13 +302,16 @@ int strv_split_extract(char ***t, const char *s, const char *separators, Extract
|
||||
l[n] = NULL;
|
||||
}
|
||||
|
||||
if (!l)
|
||||
if (!l) {
|
||||
l = new0(char*, 1);
|
||||
if (!l)
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
*t = l;
|
||||
l = NULL;
|
||||
|
||||
return 0;
|
||||
return (int) n;
|
||||
}
|
||||
|
||||
char *strv_join(char **l, const char *separator) {
|
||||
@@ -745,3 +748,41 @@ char **strv_skip(char **l, size_t n) {
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
int strv_extend_n(char ***l, const char *value, size_t n) {
|
||||
size_t i, j, k;
|
||||
char **nl;
|
||||
|
||||
assert(l);
|
||||
|
||||
if (!value)
|
||||
return 0;
|
||||
if (n == 0)
|
||||
return 0;
|
||||
|
||||
/* Adds the value value n times to l */
|
||||
|
||||
k = strv_length(*l);
|
||||
|
||||
nl = realloc(*l, sizeof(char*) * (k + n + 1));
|
||||
if (!nl)
|
||||
return -ENOMEM;
|
||||
|
||||
*l = nl;
|
||||
|
||||
for (i = k; i < k + n; i++) {
|
||||
nl[i] = strdup(value);
|
||||
if (!nl[i])
|
||||
goto rollback;
|
||||
}
|
||||
|
||||
nl[i] = NULL;
|
||||
return 0;
|
||||
|
||||
rollback:
|
||||
for (j = k; j < i; i++)
|
||||
free(nl[j]);
|
||||
|
||||
nl[k] = NULL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -158,3 +158,5 @@ static inline bool strv_fnmatch_or_empty(char* const* patterns, const char *s, i
|
||||
char ***strv_free_free(char ***l);
|
||||
|
||||
char **strv_skip(char **l, size_t n);
|
||||
|
||||
int strv_extend_n(char ***l, const char *value, size_t n);
|
||||
|
||||
@@ -6842,3 +6842,28 @@ int version(void) {
|
||||
SYSTEMD_FEATURES);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool fdname_is_valid(const char *s) {
|
||||
const char *p;
|
||||
|
||||
/* Validates a name for $LISTEN_NAMES. We basically allow
|
||||
* everything ASCII that's not a control character. Also, as
|
||||
* special exception the ":" character is not allowed, as we
|
||||
* use that as field separator in $LISTEN_NAMES.
|
||||
*
|
||||
* Note that the empty string is explicitly allowed here.*/
|
||||
|
||||
if (!s)
|
||||
return false;
|
||||
|
||||
for (p = s; *p; p++) {
|
||||
if (*p < ' ')
|
||||
return false;
|
||||
if (*p >= 127)
|
||||
return false;
|
||||
if (*p == ':')
|
||||
return false;
|
||||
}
|
||||
|
||||
return p - s < 256;
|
||||
}
|
||||
|
||||
@@ -941,3 +941,5 @@ int receive_one_fd(int transport_fd, int flags);
|
||||
void nop_signal_handler(int sig);
|
||||
|
||||
int version(void);
|
||||
|
||||
bool fdname_is_valid(const char *s);
|
||||
|
||||
@@ -84,6 +84,25 @@ static int property_get_listen(
|
||||
return sd_bus_message_close_container(reply);
|
||||
}
|
||||
|
||||
|
||||
static int property_get_fdname(
|
||||
sd_bus *bus,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *property,
|
||||
sd_bus_message *reply,
|
||||
void *userdata,
|
||||
sd_bus_error *error) {
|
||||
|
||||
Socket *s = SOCKET(userdata);
|
||||
|
||||
assert(bus);
|
||||
assert(reply);
|
||||
assert(s);
|
||||
|
||||
return sd_bus_message_append(reply, "s", socket_fdname(s));
|
||||
}
|
||||
|
||||
const sd_bus_vtable bus_socket_vtable[] = {
|
||||
SD_BUS_VTABLE_START(0),
|
||||
SD_BUS_PROPERTY("BindIPv6Only", "s", property_get_bind_ipv6_only, offsetof(Socket, bind_ipv6_only), SD_BUS_VTABLE_PROPERTY_CONST),
|
||||
@@ -128,6 +147,7 @@ const sd_bus_vtable bus_socket_vtable[] = {
|
||||
SD_BUS_PROPERTY("Result", "s", property_get_result, offsetof(Socket, result), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
|
||||
SD_BUS_PROPERTY("NConnections", "u", bus_property_get_unsigned, offsetof(Socket, n_connections), 0),
|
||||
SD_BUS_PROPERTY("NAccepted", "u", bus_property_get_unsigned, offsetof(Socket, n_accepted), 0),
|
||||
SD_BUS_PROPERTY("FileDescriptorName", "s", property_get_fdname, 0, 0),
|
||||
BUS_EXEC_COMMAND_LIST_VTABLE("ExecStartPre", offsetof(Socket, exec_command[SOCKET_EXEC_START_PRE]), SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION),
|
||||
BUS_EXEC_COMMAND_LIST_VTABLE("ExecStartPost", offsetof(Socket, exec_command[SOCKET_EXEC_START_POST]), SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION),
|
||||
BUS_EXEC_COMMAND_LIST_VTABLE("ExecStopPre", offsetof(Socket, exec_command[SOCKET_EXEC_STOP_PRE]), SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION),
|
||||
|
||||
+49
-36
@@ -21,18 +21,18 @@
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <glob.h>
|
||||
#include <grp.h>
|
||||
#include <poll.h>
|
||||
#include <glob.h>
|
||||
#include <utmpx.h>
|
||||
#include <signal.h>
|
||||
#include <string.h>
|
||||
#include <sys/personality.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#include <utmpx.h>
|
||||
|
||||
#ifdef HAVE_PAM
|
||||
#include <security/pam_appl.h>
|
||||
@@ -50,37 +50,38 @@
|
||||
#include <sys/apparmor.h>
|
||||
#endif
|
||||
|
||||
#include "barrier.h"
|
||||
#include "sd-messages.h"
|
||||
#include "rm-rf.h"
|
||||
#include "strv.h"
|
||||
#include "macro.h"
|
||||
#include "capability.h"
|
||||
#include "util.h"
|
||||
#include "log.h"
|
||||
#include "ioprio.h"
|
||||
#include "securebits.h"
|
||||
#include "namespace.h"
|
||||
#include "exit-status.h"
|
||||
#include "missing.h"
|
||||
#include "utmp-wtmp.h"
|
||||
#include "def.h"
|
||||
#include "path-util.h"
|
||||
#include "env-util.h"
|
||||
#include "fileio.h"
|
||||
#include "unit.h"
|
||||
#include "async.h"
|
||||
#include "selinux-util.h"
|
||||
#include "errno-list.h"
|
||||
|
||||
#include "af-list.h"
|
||||
#include "mkdir.h"
|
||||
#include "smack-util.h"
|
||||
#include "async.h"
|
||||
#include "barrier.h"
|
||||
#include "bus-endpoint.h"
|
||||
#include "cap-list.h"
|
||||
#include "capability.h"
|
||||
#include "def.h"
|
||||
#include "env-util.h"
|
||||
#include "errno-list.h"
|
||||
#include "exit-status.h"
|
||||
#include "fileio.h"
|
||||
#include "formats-util.h"
|
||||
#include "ioprio.h"
|
||||
#include "log.h"
|
||||
#include "macro.h"
|
||||
#include "missing.h"
|
||||
#include "mkdir.h"
|
||||
#include "namespace.h"
|
||||
#include "path-util.h"
|
||||
#include "process-util.h"
|
||||
#include "terminal-util.h"
|
||||
#include "rm-rf.h"
|
||||
#include "securebits.h"
|
||||
#include "selinux-util.h"
|
||||
#include "signal-util.h"
|
||||
#include "smack-util.h"
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
#include "unit.h"
|
||||
#include "util.h"
|
||||
#include "utmp-wtmp.h"
|
||||
|
||||
#ifdef HAVE_APPARMOR
|
||||
#include "apparmor-util.h"
|
||||
@@ -1198,6 +1199,7 @@ static void do_idle_pipe_dance(int idle_pipe[4]) {
|
||||
static int build_environment(
|
||||
const ExecContext *c,
|
||||
unsigned n_fds,
|
||||
char ** fd_names,
|
||||
usec_t watchdog_usec,
|
||||
const char *home,
|
||||
const char *username,
|
||||
@@ -1211,11 +1213,13 @@ static int build_environment(
|
||||
assert(c);
|
||||
assert(ret);
|
||||
|
||||
our_env = new0(char*, 10);
|
||||
our_env = new0(char*, 11);
|
||||
if (!our_env)
|
||||
return -ENOMEM;
|
||||
|
||||
if (n_fds > 0) {
|
||||
_cleanup_free_ char *joined = NULL;
|
||||
|
||||
if (asprintf(&x, "LISTEN_PID="PID_FMT, getpid()) < 0)
|
||||
return -ENOMEM;
|
||||
our_env[n_env++] = x;
|
||||
@@ -1223,6 +1227,15 @@ static int build_environment(
|
||||
if (asprintf(&x, "LISTEN_FDS=%u", n_fds) < 0)
|
||||
return -ENOMEM;
|
||||
our_env[n_env++] = x;
|
||||
|
||||
joined = strv_join(fd_names, ":");
|
||||
if (!joined)
|
||||
return -ENOMEM;
|
||||
|
||||
x = strjoin("LISTEN_FDNAMES=", joined, NULL);
|
||||
if (!x)
|
||||
return -ENOMEM;
|
||||
our_env[n_env++] = x;
|
||||
}
|
||||
|
||||
if (watchdog_usec > 0) {
|
||||
@@ -1273,7 +1286,7 @@ static int build_environment(
|
||||
}
|
||||
|
||||
our_env[n_env++] = NULL;
|
||||
assert(n_env <= 10);
|
||||
assert(n_env <= 11);
|
||||
|
||||
*ret = our_env;
|
||||
our_env = NULL;
|
||||
@@ -1850,7 +1863,7 @@ static int exec_child(
|
||||
#endif
|
||||
}
|
||||
|
||||
r = build_environment(context, n_fds, params->watchdog_usec, home, username, shell, &our_env);
|
||||
r = build_environment(context, n_fds, params->fd_names, params->watchdog_usec, home, username, shell, &our_env);
|
||||
if (r < 0) {
|
||||
*exit_status = EXIT_MEMORY;
|
||||
return r;
|
||||
|
||||
+12
-1
@@ -208,19 +208,30 @@ struct ExecContext {
|
||||
|
||||
struct ExecParameters {
|
||||
char **argv;
|
||||
int *fds; unsigned n_fds;
|
||||
|
||||
int *fds;
|
||||
char **fd_names;
|
||||
unsigned n_fds;
|
||||
|
||||
char **environment;
|
||||
|
||||
bool apply_permissions;
|
||||
bool apply_chroot;
|
||||
bool apply_tty_stdin;
|
||||
|
||||
bool confirm_spawn;
|
||||
bool selinux_context_net;
|
||||
|
||||
CGroupMask cgroup_supported;
|
||||
const char *cgroup_path;
|
||||
bool cgroup_delegate;
|
||||
|
||||
const char *runtime_prefix;
|
||||
|
||||
usec_t watchdog_usec;
|
||||
|
||||
int *idle_pipe;
|
||||
|
||||
char *bus_endpoint_path;
|
||||
int bus_endpoint_fd;
|
||||
};
|
||||
|
||||
@@ -287,6 +287,7 @@ Socket.MessageQueueMaxMessages, config_parse_long, 0,
|
||||
Socket.MessageQueueMessageSize, config_parse_long, 0, offsetof(Socket, mq_msgsize)
|
||||
Socket.RemoveOnStop, config_parse_bool, 0, offsetof(Socket, remove_on_stop)
|
||||
Socket.Symlinks, config_parse_unit_path_strv_printf, 0, offsetof(Socket, symlinks)
|
||||
Socket.FileDescriptorName, config_parse_fdname, 0, 0
|
||||
Socket.Service, config_parse_socket_service, 0, 0
|
||||
m4_ifdef(`HAVE_SMACK',
|
||||
`Socket.SmackLabel, config_parse_string, 0, offsetof(Socket, smack)
|
||||
|
||||
@@ -1475,10 +1475,10 @@ int config_parse_socket_service(
|
||||
void *userdata) {
|
||||
|
||||
_cleanup_bus_error_free_ sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
Socket *s = data;
|
||||
int r;
|
||||
Unit *x;
|
||||
_cleanup_free_ char *p = NULL;
|
||||
Socket *s = data;
|
||||
Unit *x;
|
||||
int r;
|
||||
|
||||
assert(filename);
|
||||
assert(lvalue);
|
||||
@@ -1507,6 +1507,50 @@ int config_parse_socket_service(
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_parse_fdname(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
unsigned line,
|
||||
const char *section,
|
||||
unsigned section_line,
|
||||
const char *lvalue,
|
||||
int ltype,
|
||||
const char *rvalue,
|
||||
void *data,
|
||||
void *userdata) {
|
||||
|
||||
_cleanup_free_ char *p = NULL;
|
||||
Socket *s = data;
|
||||
int r;
|
||||
|
||||
assert(filename);
|
||||
assert(lvalue);
|
||||
assert(rvalue);
|
||||
assert(data);
|
||||
|
||||
if (isempty(rvalue)) {
|
||||
s->fdname = mfree(s->fdname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
r = unit_name_printf(UNIT(s), rvalue, &p);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_ERR, filename, line, r, "Failed to resolve specifiers, ignoring: %s", rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!fdname_is_valid(p)) {
|
||||
log_syntax(unit, LOG_ERR, filename, line, 0, "Invalid file descriptor name, ignoring: %s", p);
|
||||
return 0;
|
||||
}
|
||||
|
||||
free(s->fdname);
|
||||
s->fdname = p;
|
||||
p = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_parse_service_sockets(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
|
||||
@@ -107,6 +107,7 @@ int config_parse_protect_system(const char* unit, const char *filename, unsigned
|
||||
int config_parse_bus_name(const char* unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
|
||||
int config_parse_exec_utmp_mode(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
|
||||
int config_parse_working_directory(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
|
||||
int config_parse_fdname(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
|
||||
|
||||
/* gperf prototypes */
|
||||
const struct ConfigPerfItem* load_fragment_gperf_lookup(const char *key, unsigned length);
|
||||
|
||||
@@ -495,6 +495,7 @@ static void manager_clean_environment(Manager *m) {
|
||||
"MANAGERPID",
|
||||
"LISTEN_PID",
|
||||
"LISTEN_FDS",
|
||||
"LISTEN_FDNAMES",
|
||||
"WATCHDOG_PID",
|
||||
"WATCHDOG_USEC",
|
||||
NULL);
|
||||
|
||||
+123
-58
@@ -261,6 +261,7 @@ static void service_fd_store_unlink(ServiceFDStore *fs) {
|
||||
sd_event_source_unref(fs->event_source);
|
||||
}
|
||||
|
||||
free(fs->fdname);
|
||||
safe_close(fs->fd);
|
||||
free(fs);
|
||||
}
|
||||
@@ -334,7 +335,7 @@ static int on_fd_store_io(sd_event_source *e, int fd, uint32_t revents, void *us
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int service_add_fd_store(Service *s, int fd) {
|
||||
static int service_add_fd_store(Service *s, int fd, const char *name) {
|
||||
ServiceFDStore *fs;
|
||||
int r;
|
||||
|
||||
@@ -361,9 +362,13 @@ static int service_add_fd_store(Service *s, int fd) {
|
||||
|
||||
fs->fd = fd;
|
||||
fs->service = s;
|
||||
fs->fdname = strdup(name ?: "stored");
|
||||
if (!fs->fdname)
|
||||
return -ENOMEM;
|
||||
|
||||
r = sd_event_add_io(UNIT(s)->manager->event, &fs->event_source, fd, 0, on_fd_store_io, fs);
|
||||
if (r < 0) {
|
||||
free(fs->fdname);
|
||||
free(fs);
|
||||
return r;
|
||||
}
|
||||
@@ -376,7 +381,7 @@ static int service_add_fd_store(Service *s, int fd) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int service_add_fd_store_set(Service *s, FDSet *fds) {
|
||||
static int service_add_fd_store_set(Service *s, FDSet *fds, const char *name) {
|
||||
int r;
|
||||
|
||||
assert(s);
|
||||
@@ -391,7 +396,7 @@ static int service_add_fd_store_set(Service *s, FDSet *fds) {
|
||||
if (fd < 0)
|
||||
break;
|
||||
|
||||
r = service_add_fd_store(s, fd);
|
||||
r = service_add_fd_store(s, fd, name);
|
||||
if (r < 0)
|
||||
return log_unit_error_errno(UNIT(s), r, "Couldn't add fd to fd store: %m");
|
||||
if (r > 0) {
|
||||
@@ -957,56 +962,79 @@ static int service_coldplug(Unit *u) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int service_collect_fds(Service *s, int **fds) {
|
||||
static int service_collect_fds(Service *s, int **fds, char ***fd_names) {
|
||||
_cleanup_strv_free_ char **rfd_names = NULL;
|
||||
_cleanup_free_ int *rfds = NULL;
|
||||
int rn_fds = 0;
|
||||
Iterator i;
|
||||
Unit *u;
|
||||
int rn_fds = 0, r;
|
||||
|
||||
assert(s);
|
||||
assert(fds);
|
||||
assert(fd_names);
|
||||
|
||||
if (s->socket_fd >= 0)
|
||||
return -EINVAL;
|
||||
if (s->socket_fd >= 0) {
|
||||
|
||||
SET_FOREACH(u, UNIT(s)->dependencies[UNIT_TRIGGERED_BY], i) {
|
||||
_cleanup_free_ int *cfds = NULL;
|
||||
Socket *sock;
|
||||
int cn_fds;
|
||||
/* Pass the per-connection socket */
|
||||
|
||||
if (u->type != UNIT_SOCKET)
|
||||
continue;
|
||||
rfds = new(int, 1);
|
||||
if (!rfds)
|
||||
return -ENOMEM;
|
||||
rfds[0] = s->socket_fd;
|
||||
|
||||
sock = SOCKET(u);
|
||||
rfd_names = strv_new("connection", NULL);
|
||||
if (!rfd_names)
|
||||
return -ENOMEM;
|
||||
|
||||
cn_fds = socket_collect_fds(sock, &cfds);
|
||||
if (cn_fds < 0)
|
||||
return cn_fds;
|
||||
rn_fds = 1;
|
||||
} else {
|
||||
Iterator i;
|
||||
Unit *u;
|
||||
|
||||
if (cn_fds <= 0)
|
||||
continue;
|
||||
/* Pass all our configured sockets for singleton services */
|
||||
|
||||
if (!rfds) {
|
||||
rfds = cfds;
|
||||
rn_fds = cn_fds;
|
||||
SET_FOREACH(u, UNIT(s)->dependencies[UNIT_TRIGGERED_BY], i) {
|
||||
_cleanup_free_ int *cfds = NULL;
|
||||
Socket *sock;
|
||||
int cn_fds;
|
||||
|
||||
cfds = NULL;
|
||||
} else {
|
||||
int *t;
|
||||
if (u->type != UNIT_SOCKET)
|
||||
continue;
|
||||
|
||||
t = realloc(rfds, (rn_fds + cn_fds) * sizeof(int));
|
||||
if (!t)
|
||||
return -ENOMEM;
|
||||
sock = SOCKET(u);
|
||||
|
||||
memcpy(t + rn_fds, cfds, cn_fds * sizeof(int));
|
||||
cn_fds = socket_collect_fds(sock, &cfds);
|
||||
if (cn_fds < 0)
|
||||
return cn_fds;
|
||||
|
||||
rfds = t;
|
||||
rn_fds += cn_fds;
|
||||
if (cn_fds <= 0)
|
||||
continue;
|
||||
|
||||
if (!rfds) {
|
||||
rfds = cfds;
|
||||
rn_fds = cn_fds;
|
||||
|
||||
cfds = NULL;
|
||||
} else {
|
||||
int *t;
|
||||
|
||||
t = realloc(rfds, (rn_fds + cn_fds) * sizeof(int));
|
||||
if (!t)
|
||||
return -ENOMEM;
|
||||
|
||||
memcpy(t + rn_fds, cfds, cn_fds * sizeof(int));
|
||||
|
||||
rfds = t;
|
||||
rn_fds += cn_fds;
|
||||
}
|
||||
|
||||
r = strv_extend_n(&rfd_names, socket_fdname(sock), cn_fds);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
if (s->n_fd_store > 0) {
|
||||
ServiceFDStore *fs;
|
||||
char **nl;
|
||||
int *t;
|
||||
|
||||
t = realloc(rfds, (rn_fds + s->n_fd_store) * sizeof(int));
|
||||
@@ -1014,12 +1042,30 @@ static int service_collect_fds(Service *s, int **fds) {
|
||||
return -ENOMEM;
|
||||
|
||||
rfds = t;
|
||||
LIST_FOREACH(fd_store, fs, s->fd_store)
|
||||
rfds[rn_fds++] = fs->fd;
|
||||
|
||||
nl = realloc(rfd_names, (rn_fds + s->n_fd_store + 1) * sizeof(char*));
|
||||
if (!nl)
|
||||
return -ENOMEM;
|
||||
|
||||
rfd_names = nl;
|
||||
|
||||
LIST_FOREACH(fd_store, fs, s->fd_store) {
|
||||
rfds[rn_fds] = fs->fd;
|
||||
rfd_names[rn_fds] = strdup(strempty(fs->fdname));
|
||||
if (!rfd_names[rn_fds])
|
||||
return -ENOMEM;
|
||||
|
||||
rn_fds++;
|
||||
}
|
||||
|
||||
rfd_names[rn_fds] = NULL;
|
||||
}
|
||||
|
||||
*fds = rfds;
|
||||
*fd_names = rfd_names;
|
||||
|
||||
rfds = NULL;
|
||||
rfd_names = NULL;
|
||||
|
||||
return rn_fds;
|
||||
}
|
||||
@@ -1035,15 +1081,13 @@ static int service_spawn(
|
||||
bool is_control,
|
||||
pid_t *_pid) {
|
||||
|
||||
pid_t pid;
|
||||
int r;
|
||||
int *fds = NULL;
|
||||
_cleanup_free_ int *fdsbuf = NULL;
|
||||
unsigned n_fds = 0, n_env = 0;
|
||||
_cleanup_strv_free_ char **argv = NULL, **final_env = NULL, **our_env = NULL, **fd_names = NULL;
|
||||
_cleanup_free_ char *bus_endpoint_path = NULL;
|
||||
_cleanup_strv_free_ char
|
||||
**argv = NULL, **final_env = NULL, **our_env = NULL;
|
||||
_cleanup_free_ int *fds = NULL;
|
||||
unsigned n_fds = 0, n_env = 0;
|
||||
const char *path;
|
||||
pid_t pid;
|
||||
|
||||
ExecParameters exec_params = {
|
||||
.apply_permissions = apply_permissions,
|
||||
.apply_chroot = apply_chroot,
|
||||
@@ -1052,6 +1096,8 @@ static int service_spawn(
|
||||
.selinux_context_net = s->socket_fd_selinux_context_net
|
||||
};
|
||||
|
||||
int r;
|
||||
|
||||
assert(s);
|
||||
assert(c);
|
||||
assert(_pid);
|
||||
@@ -1071,17 +1117,11 @@ static int service_spawn(
|
||||
s->exec_context.std_output == EXEC_OUTPUT_SOCKET ||
|
||||
s->exec_context.std_error == EXEC_OUTPUT_SOCKET) {
|
||||
|
||||
if (s->socket_fd >= 0) {
|
||||
fds = &s->socket_fd;
|
||||
n_fds = 1;
|
||||
} else {
|
||||
r = service_collect_fds(s, &fdsbuf);
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
r = service_collect_fds(s, &fds, &fd_names);
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
|
||||
fds = fdsbuf;
|
||||
n_fds = r;
|
||||
}
|
||||
n_fds = r;
|
||||
}
|
||||
|
||||
if (timeout > 0) {
|
||||
@@ -1119,7 +1159,7 @@ static int service_spawn(
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (UNIT_DEREF(s->accept_socket)) {
|
||||
if (s->socket_fd >= 0) {
|
||||
union sockaddr_union sa;
|
||||
socklen_t salen = sizeof(sa);
|
||||
|
||||
@@ -1185,6 +1225,7 @@ static int service_spawn(
|
||||
|
||||
exec_params.argv = argv;
|
||||
exec_params.fds = fds;
|
||||
exec_params.fd_names = fd_names;
|
||||
exec_params.n_fds = n_fds;
|
||||
exec_params.environment = final_env;
|
||||
exec_params.confirm_spawn = UNIT(s)->manager->confirm_spawn;
|
||||
@@ -2047,13 +2088,16 @@ static int service_serialize(Unit *u, FILE *f, FDSet *fds) {
|
||||
}
|
||||
|
||||
LIST_FOREACH(fd_store, fs, s->fd_store) {
|
||||
_cleanup_free_ char *c = NULL;
|
||||
int copy;
|
||||
|
||||
copy = fdset_put_dup(fds, fs->fd);
|
||||
if (copy < 0)
|
||||
return copy;
|
||||
|
||||
unit_serialize_item_format(u, f, "fd-store-fd", "%i", copy);
|
||||
c = cescape(fs->fdname);
|
||||
|
||||
unit_serialize_item_format(u, f, "fd-store-fd", "%i %s", copy, strempty(c));
|
||||
}
|
||||
|
||||
if (s->main_exec_status.pid > 0) {
|
||||
@@ -2183,12 +2227,24 @@ static int service_deserialize_item(Unit *u, const char *key, const char *value,
|
||||
s->bus_endpoint_fd = fdset_remove(fds, fd);
|
||||
}
|
||||
} else if (streq(key, "fd-store-fd")) {
|
||||
const char *fdv;
|
||||
size_t pf;
|
||||
int fd;
|
||||
|
||||
if (safe_atoi(value, &fd) < 0 || fd < 0 || !fdset_contains(fds, fd))
|
||||
pf = strcspn(value, WHITESPACE);
|
||||
fdv = strndupa(value, pf);
|
||||
|
||||
if (safe_atoi(fdv, &fd) < 0 || fd < 0 || !fdset_contains(fds, fd))
|
||||
log_unit_debug(u, "Failed to parse fd-store-fd value: %s", value);
|
||||
else {
|
||||
r = service_add_fd_store(s, fd);
|
||||
_cleanup_free_ char *t = NULL;
|
||||
const char *fdn;
|
||||
|
||||
fdn = value + pf;
|
||||
fdn += strspn(fdn, WHITESPACE);
|
||||
(void) cunescape(fdn, 0, &t);
|
||||
|
||||
r = service_add_fd_store(s, fd, t);
|
||||
if (r < 0)
|
||||
log_unit_error_errno(u, r, "Failed to add fd to store: %m");
|
||||
else if (r > 0)
|
||||
@@ -2942,8 +2998,17 @@ static void service_notify_message(Unit *u, pid_t pid, char **tags, FDSet *fds)
|
||||
if (strv_find(tags, "WATCHDOG=1"))
|
||||
service_reset_watchdog(s);
|
||||
|
||||
if (strv_find(tags, "FDSTORE=1"))
|
||||
service_add_fd_store_set(s, fds);
|
||||
if (strv_find(tags, "FDSTORE=1")) {
|
||||
const char *name;
|
||||
|
||||
name = strv_find_startswith(tags, "FDNAME=");
|
||||
if (name && !fdname_is_valid(name)) {
|
||||
log_unit_warning(u, "Passed FDNAME= name is invalid, ignoring.");
|
||||
name = NULL;
|
||||
}
|
||||
|
||||
service_add_fd_store_set(s, fds, name);
|
||||
}
|
||||
|
||||
/* Notify clients about changed status or main pid */
|
||||
if (notify_dbus)
|
||||
|
||||
@@ -97,6 +97,7 @@ struct ServiceFDStore {
|
||||
Service *service;
|
||||
|
||||
int fd;
|
||||
char *fdname;
|
||||
sd_event_source *event_source;
|
||||
|
||||
LIST_FIELDS(ServiceFDStore, fd_store);
|
||||
|
||||
@@ -508,6 +508,7 @@ static void socket_dump(Unit *u, FILE *f, const char *prefix) {
|
||||
"%sTCPCongestion: %s\n"
|
||||
"%sRemoveOnStop: %s\n"
|
||||
"%sWritable: %s\n"
|
||||
"%sFDName: %s\n"
|
||||
"%sSELinuxContextFromNet: %s\n",
|
||||
prefix, socket_state_to_string(s->state),
|
||||
prefix, socket_result_to_string(s->result),
|
||||
@@ -525,6 +526,7 @@ static void socket_dump(Unit *u, FILE *f, const char *prefix) {
|
||||
prefix, strna(s->tcp_congestion),
|
||||
prefix, yes_no(s->remove_on_stop),
|
||||
prefix, yes_no(s->writable),
|
||||
prefix, socket_fdname(s),
|
||||
prefix, yes_no(s->selinux_context_from_net));
|
||||
|
||||
if (s->control_pid > 0)
|
||||
@@ -2760,6 +2762,19 @@ static int socket_get_timeout(Unit *u, uint64_t *timeout) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
char *socket_fdname(Socket *s) {
|
||||
assert(s);
|
||||
|
||||
/* Returns the name to use for $LISTEN_NAMES. If the user
|
||||
* didn't specify anything specifically, use the socket unit's
|
||||
* name as fallback. */
|
||||
|
||||
if (s->fdname)
|
||||
return s->fdname;
|
||||
|
||||
return UNIT(s)->id;
|
||||
}
|
||||
|
||||
static const char* const socket_exec_command_table[_SOCKET_EXEC_COMMAND_MAX] = {
|
||||
[SOCKET_EXEC_START_PRE] = "StartPre",
|
||||
[SOCKET_EXEC_START_CHOWN] = "StartChown",
|
||||
|
||||
+6
-2
@@ -154,6 +154,8 @@ struct Socket {
|
||||
char *user, *group;
|
||||
|
||||
bool reset_cpu_usage:1;
|
||||
|
||||
char *fdname;
|
||||
};
|
||||
|
||||
/* Called from the service code when collecting fds */
|
||||
@@ -164,6 +166,10 @@ void socket_connection_unref(Socket *s);
|
||||
|
||||
void socket_free_ports(Socket *s);
|
||||
|
||||
int socket_instantiate_service(Socket *s);
|
||||
|
||||
char *socket_fdname(Socket *s);
|
||||
|
||||
extern const UnitVTable socket_vtable;
|
||||
|
||||
const char* socket_exec_command_to_string(SocketExecCommand i) _const_;
|
||||
@@ -173,5 +179,3 @@ const char* socket_result_to_string(SocketResult i) _const_;
|
||||
SocketResult socket_result_from_string(const char *s) _pure_;
|
||||
|
||||
const char* socket_port_type_to_string(SocketPort *p) _pure_;
|
||||
|
||||
int socket_instantiate_service(Socket *s);
|
||||
|
||||
@@ -19,25 +19,37 @@
|
||||
along with systemd; If not, see <http://www.gnu.org/licenses/>.
|
||||
***/
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
#include <limits.h>
|
||||
#include <mqueue.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "util.h"
|
||||
#include "path-util.h"
|
||||
#include "socket-util.h"
|
||||
#include "strv.h"
|
||||
#include "util.h"
|
||||
|
||||
#include "sd-daemon.h"
|
||||
|
||||
static void unsetenv_all(bool unset_environment) {
|
||||
|
||||
if (!unset_environment)
|
||||
return;
|
||||
|
||||
unsetenv("LISTEN_PID");
|
||||
unsetenv("LISTEN_FDS");
|
||||
unsetenv("LISTEN_FDNAMES");
|
||||
}
|
||||
|
||||
_public_ int sd_listen_fds(int unset_environment) {
|
||||
const char *e;
|
||||
unsigned n;
|
||||
@@ -79,12 +91,49 @@ _public_ int sd_listen_fds(int unset_environment) {
|
||||
r = (int) n;
|
||||
|
||||
finish:
|
||||
if (unset_environment) {
|
||||
unsetenv("LISTEN_PID");
|
||||
unsetenv("LISTEN_FDS");
|
||||
unsetenv_all(unset_environment);
|
||||
return r;
|
||||
}
|
||||
|
||||
_public_ int sd_listen_fds_with_names(int unset_environment, char ***names) {
|
||||
_cleanup_strv_free_ char **l = NULL;
|
||||
bool have_names;
|
||||
int n_names = 0, n_fds;
|
||||
const char *e;
|
||||
int r;
|
||||
|
||||
if (!names)
|
||||
return sd_listen_fds(unset_environment);
|
||||
|
||||
e = getenv("LISTEN_FDNAMES");
|
||||
if (e) {
|
||||
n_names = strv_split_extract(&l, e, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
|
||||
if (n_names < 0) {
|
||||
unsetenv_all(unset_environment);
|
||||
return n_names;
|
||||
}
|
||||
|
||||
have_names = true;
|
||||
} else
|
||||
have_names = false;
|
||||
|
||||
n_fds = sd_listen_fds(unset_environment);
|
||||
if (n_fds <= 0)
|
||||
return n_fds;
|
||||
|
||||
if (have_names) {
|
||||
if (n_names != n_fds)
|
||||
return -EINVAL;
|
||||
} else {
|
||||
r = strv_extend_n(&l, "unknown", n_fds);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return r;
|
||||
*names = l;
|
||||
l = NULL;
|
||||
|
||||
return n_fds;
|
||||
}
|
||||
|
||||
_public_ int sd_is_fifo(int fd, const char *path) {
|
||||
|
||||
@@ -76,6 +76,8 @@ _SD_BEGIN_DECLARATIONS;
|
||||
*/
|
||||
int sd_listen_fds(int unset_environment);
|
||||
|
||||
int sd_listen_fds_with_names(int unset_environment, char ***names);
|
||||
|
||||
/*
|
||||
Helper call for identifying a passed file descriptor. Returns 1 if
|
||||
the file descriptor is a FIFO in the file system stored under the
|
||||
|
||||
+15
-2
@@ -21,9 +21,22 @@
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include "systemd/sd-daemon.h"
|
||||
#include "sd-daemon.h"
|
||||
|
||||
#include "strv.h"
|
||||
|
||||
int main(int argc, char*argv[]) {
|
||||
_cleanup_strv_free_ char **l = NULL;
|
||||
int n, i;
|
||||
|
||||
n = sd_listen_fds_with_names(false, &l);
|
||||
if (n < 0) {
|
||||
log_error_errno(n, "Failed to get listening fds: %m");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
for (i = 0; i < n; i++)
|
||||
log_info("fd=%i name=%s\n", SD_LISTEN_FDS_START + i, l[i]);
|
||||
|
||||
sd_notify(0,
|
||||
"STATUS=Starting up");
|
||||
@@ -49,5 +62,5 @@ int main(int argc, char*argv[]) {
|
||||
"STOPPING=1");
|
||||
sleep(5);
|
||||
|
||||
return 0;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
+32
-4
@@ -155,7 +155,7 @@ static void test_strv_join(void) {
|
||||
|
||||
static void test_strv_quote_unquote(const char* const *split, const char *quoted) {
|
||||
_cleanup_free_ char *p;
|
||||
_cleanup_strv_free_ char **s;
|
||||
_cleanup_strv_free_ char **s = NULL;
|
||||
char **t;
|
||||
int r;
|
||||
|
||||
@@ -166,7 +166,7 @@ static void test_strv_quote_unquote(const char* const *split, const char *quoted
|
||||
assert_se(streq(p, quoted));
|
||||
|
||||
r = strv_split_extract(&s, quoted, WHITESPACE, EXTRACT_QUOTES);
|
||||
assert_se(r == 0);
|
||||
assert_se(r == (int) strv_length(s));
|
||||
assert_se(s);
|
||||
STRV_FOREACH(t, s) {
|
||||
assert_se(*t);
|
||||
@@ -183,7 +183,7 @@ static void test_strv_unquote(const char *quoted, char **list) {
|
||||
int r;
|
||||
|
||||
r = strv_split_extract(&s, quoted, WHITESPACE, EXTRACT_QUOTES);
|
||||
assert_se(r == 0);
|
||||
assert_se(r == (int) strv_length(list));
|
||||
assert_se(s);
|
||||
j = strv_join(s, " | ");
|
||||
assert_se(j);
|
||||
@@ -225,7 +225,7 @@ static void test_strv_split_extract(void) {
|
||||
int r;
|
||||
|
||||
r = strv_split_extract(&l, str, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
|
||||
assert_se(r == 0);
|
||||
assert_se(r == (int) strv_length(l));
|
||||
assert_se(streq_ptr(l[0], ""));
|
||||
assert_se(streq_ptr(l[1], "foo:bar"));
|
||||
assert_se(streq_ptr(l[2], ""));
|
||||
@@ -591,6 +591,33 @@ static void test_strv_skip(void) {
|
||||
test_strv_skip_one(STRV_MAKE(NULL), 55, STRV_MAKE(NULL));
|
||||
}
|
||||
|
||||
static void test_strv_extend_n(void) {
|
||||
_cleanup_strv_free_ char **v = NULL;
|
||||
|
||||
v = strv_new("foo", "bar", NULL);
|
||||
assert_se(v);
|
||||
|
||||
assert_se(strv_extend_n(&v, "waldo", 3) >= 0);
|
||||
assert_se(strv_extend_n(&v, "piep", 2) >= 0);
|
||||
|
||||
assert_se(streq(v[0], "foo"));
|
||||
assert_se(streq(v[1], "bar"));
|
||||
assert_se(streq(v[2], "waldo"));
|
||||
assert_se(streq(v[3], "waldo"));
|
||||
assert_se(streq(v[4], "waldo"));
|
||||
assert_se(streq(v[5], "piep"));
|
||||
assert_se(streq(v[6], "piep"));
|
||||
assert_se(v[7] == NULL);
|
||||
|
||||
v = strv_free(v);
|
||||
|
||||
assert_se(strv_extend_n(&v, "foo", 1) >= 0);
|
||||
assert_se(strv_extend_n(&v, "bar", 0) >= 0);
|
||||
|
||||
assert_se(streq(v[0], "foo"));
|
||||
assert_se(v[1] == NULL);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
test_specifier_printf();
|
||||
test_strv_foreach();
|
||||
@@ -650,6 +677,7 @@ int main(int argc, char *argv[]) {
|
||||
test_strv_reverse();
|
||||
test_strv_shell_escape();
|
||||
test_strv_skip();
|
||||
test_strv_extend_n();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user