diff --git a/doc/manpages/dinit-monitor.8.m4 b/doc/manpages/dinit-monitor.8.m4 index 2cdd48b3..6de49e31 100644 --- a/doc/manpages/dinit-monitor.8.m4 +++ b/doc/manpages/dinit-monitor.8.m4 @@ -1,7 +1,7 @@ changequote(`@@@',`$$$')dnl @@@.TH DINIT\-MONITOR "8" "$$$MONTH YEAR@@@" "Dinit $$$VERSION@@@" "Dinit \- service management system" .SH NAME -dinit\-monitor \- monitor services supervised by Dinit +dinit\-monitor \- monitor services or environment supervised by Dinit .\" .SH SYNOPSIS .\" @@ -9,15 +9,17 @@ dinit\-monitor \- monitor services supervised by Dinit .nh .HP .B dinit-monitor -[\fIoptions\fR] {\fB\-c\fR \fIcommand\fR, \fB\-\-command\fR \fIcommand\fR} \fIservice-name\fR [\fIservice-name\fR...] +[\fIoptions\fR] {\fB\-c\fR \fIcommand\fR, \fB\-\-command\fR \fIcommand\fR} \fIservice-or-env\fR [\fIservice-or-env\fR...] .\" .PD .hy .\" .SH DESCRIPTION .\" -\fBdinit\-monitor\fR is a utility to monitor the state of one or more services managed by the \fBdinit\fR daemon. -Changes in service state are reported by the execution of the specified command. +\fBdinit\-monitor\fR is a utility to monitor the state of one or more services or environment managed by the \fBdinit\fR daemon. +Changes in service or environment state are reported by the execution of the specified command. +When monitoring environment, positional arguments are not required (all environment will be monitored). +By default, service events are monitored. .\" .SH GENERAL OPTIONS .TP @@ -27,6 +29,11 @@ Display brief help text and then exit. \fB\-\-version\fR Display version and then exit. .TP +\fB\-e\fR, \fB\-\-exit\fR +Exit after the first command is executed, instead of waiting for more events. +\fB\-E\fR, \fB\-\-env\fR +Instead of monitoring the services, monitor changes in the global environment. +If no environment variables are passed, all environment is monitored. \fB\-s\fR, \fB\-\-system\fR Control the system init process (this is the default when run as root). This option determines the default path to the control socket used to communicate with the \fBdinit\fR daemon @@ -38,8 +45,8 @@ This option determines the default path to the control socket used to communicat (it does not override the \fB\-p\fR option). .TP \fB\-i\fR, \fB\-\-initial\fR -Issue the specified command additionally for the initial status of the services (when \fBdinit\-monitor\fR is started). -Without this option, the command is only executed whenever service status changes. +Issue the specified command additionally for the initial status of the services or environment (when \fBdinit\-monitor\fR is started). +Without this option, the command is only executed whenever status changes. .TP \fB\-\-str\-started\fR \fIstarted-text\fR Specify the text used for the substitution of the status in the command (as specified @@ -53,6 +60,14 @@ by the \fB\-\-command\fR option) when a service stops. Specify the text used for the substitution of the status in the command (as specified by the \fB\-\-command\fR option) when a service fails to start. .TP +\fB\-\-str\-set\fR \fIset-text\fR +Specify the text used for the substitution of the status in the command (as specified +by the \fB\-\-command\fR option) when an environment variable is set. +.TP +\fB\-\-str\-unset\fR \fIunset-text\fR +Specify the text used for the substitution of the status in the command (as specified +by the \fB\-\-command\fR option) when an environment variable is unset. +.TP \fB\-\-socket\-path\fR \fIsocket-path\fR, \fB\-p\fR \fIsocket-path\fR Specify the path to the socket used for communicating with the service manager daemon. When not specified, the \fIDINIT_SOCKET_PATH\fR environment variable is read, otherwise @@ -61,9 +76,11 @@ Dinit's default values are used. .SH STATUS REPORT OPTIONS .TP \fB\-\-command\fR \fIcommand\fR, \fB\-c\fR \fIcommand\fR -Execute the specified \fIcommand\fR when the service status changes. In \fIcommand\fR, \fB%n\fR -will be substituted with the service name and \fB%s\fR will be substituted with a textual -description of the new status (\fBstarted\fR, \fBstopped\fR or \fBfailed\fR). A double percent sign +Execute the specified \fIcommand\fR when the service status changes. +In \fIcommand\fR, \fB%n\fR will be substituted with the service or environment variable name, +\fB%v\fR will be substituted with the environment variable value, and \fB%s\fR will be substituted +with a textual description of the new status (\fBstarted\fR, \fBstopped\fR or \fBfailed\fR for +services, \fBset\fR or \fBunset\fR for environment variables). A double percent sign (\fB%%\fR) is substituted with a single percent sign character. .\" .SH OPERATION diff --git a/doc/manpages/dinitctl.8.m4 b/doc/manpages/dinitctl.8.m4 index e148de87..1be95bc8 100644 --- a/doc/manpages/dinitctl.8.m4 +++ b/doc/manpages/dinitctl.8.m4 @@ -69,6 +69,9 @@ dinitctl \- control services supervised by Dinit [\fIoptions\fR] \fBsetenv\fR [\fIname\fR[=\fIvalue\fR] \fI...\fR] .HP .B dinitctl +[\fIoptions\fR] \fBunsetenv\fR [\fIname\fR \fI...\fR] +.HP +.B dinitctl [\fIoptions\fR] \fBcatlog\fR [\fB--clear\fR] \fIservice-name\fR .HP .B dinitctl @@ -321,6 +324,10 @@ called in. Any subsequently started or restarted service will have these environment variables available. This is particularly useful for user services that need access to session information. .TP +\fBunsetenv\fR +Unset one or more variables in the activation environment. +Any subsequently started or restarted service will have these environment variables unset. +.TP \fBcatlog\fR Show the contents of the log buffer for the named service. This is possible only if the log type of the service is set to \fBbuffer\fR. diff --git a/src/control.cc b/src/control.cc index babbebdb..48c91b93 100644 --- a/src/control.cc +++ b/src/control.cc @@ -111,6 +111,8 @@ bool control_conn_t::process_packet() return process_setenv(); case cp_cmd::GETALLENV: return process_getallenv(); + case cp_cmd::LISTENENV: + return process_listenenv(); case cp_cmd::SETTRIGGER: return process_set_trigger(); case cp_cmd::CATLOG: @@ -1076,13 +1078,19 @@ bool control_conn_t::process_setenv() envVar = rbuf.extract_string(3, envvar_len); eq = envVar.find('='); - if (!eq || eq == envVar.npos) { - // Not found or at the beginning of the string + if (eq == envVar.npos) { + // Unset the env var + main_env.undefine_var(std::move(envVar), true); + } + else if (eq) { + // Regular set + main_env.set_var(std::move(envVar), true); + } + else { + // At the beginning of the string goto badreq; } - main_env.set_var(std::move(envVar)); - // Success response if (!queue_packet(okRep, 1)) return false; @@ -1141,6 +1149,17 @@ bool control_conn_t::process_getallenv() return true; } +bool control_conn_t::process_listenenv() +{ + // 1 byte packet type, nothing else + rbuf.consume(1); + + main_env.add_listener(this); + + char ack_rep[] = { (char)cp_rply::ACK }; + return queue_packet(ack_rep, 1); +} + bool control_conn_t::process_set_trigger() { // 1 byte packet type @@ -1468,6 +1487,28 @@ void control_conn_t::service_event(service_record *service, service_event_t even } } +void control_conn_t::environ_event(environment *env, std::string const &var_and_val, bool overridden) noexcept +{ + // packet type (byte) + packet length (byte) + flags byte + data size + data + // flags byte can be 1 or 0 for now, 1 if the var was overridden and 0 if fresh + constexpr int pktsize = 3 + sizeof(envvar_len_t); + envvar_len_t ln = var_and_val.size() + 1; + auto *ptr = var_and_val.data(); + + try { + std::vector pkt; + pkt.reserve(pktsize + ln); + pkt.push_back((char)cp_info::ENVEVENT); + pkt.push_back(pktsize); + pkt.push_back(overridden ? 1 : 0); + pkt.insert(pkt.end(), (char *)&ln, ((char *)&ln) + sizeof(envvar_len_t)); + pkt.insert(pkt.end(), ptr, ptr + ln); + queue_packet(std::move(pkt)); + } catch (std::bad_alloc &exc) { + do_oom_close(); + } +} + bool control_conn_t::queue_packet(const char *pkt, unsigned size) noexcept { bool was_empty = outbuf.empty(); @@ -1671,6 +1712,7 @@ control_conn_t::~control_conn_t() noexcept for (auto p : service_key_map) { p.first->remove_listener(this); } + main_env.remove_listener(this); active_control_conns--; } diff --git a/src/dinit-monitor.cc b/src/dinit-monitor.cc index f5109c4e..e4596c42 100644 --- a/src/dinit-monitor.cc +++ b/src/dinit-monitor.cc @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -32,10 +33,13 @@ struct stringview { static std::vector split_command(const char *cmd); static bool load_service(int socknum, cpbuffer_t &rbuffer, const char *name, handle_t *handle, service_state_t *state); +static void request_environ(int socknum, cpbuffer_t &rbuffer); +static size_t get_allenv(int socknum, cpbuffer_t &rbuffer); // dummy handler, so we can wait for children static void sigchld_handler(int) { } -void issue_command(const char* service_name, const char* event_str,std::vector &command_parts); +static void issue_command(const char* name, const char* value, const char* event, std::vector &command_parts, bool is_env = false); +static size_t read_and_issue(int socknum, cpbuffer_t &rbuffer, size_t dsz, const std::unordered_set &varset, std::string &enval, std::vector &command_parts, bool &issued, const char* str_set, const char* str_unset); int dinit_monitor_main(int argc, char **argv) { @@ -44,9 +48,13 @@ int dinit_monitor_main(int argc, char **argv) const char *control_socket_path = nullptr; bool user_dinit = (getuid() != 0); // communicate with user daemon bool issue_init = false; // request initial service state + bool use_environ = false; // listening on activation environment changes + bool exit_after = false; // exit after first issued command const char *str_started = "started"; const char *str_stopped = "stopped"; const char *str_failed = "failed"; + const char *str_set = "set"; + const char *str_unset = "unset"; const char *command_str = nullptr; std::vector services; @@ -61,6 +69,12 @@ int dinit_monitor_main(int argc, char **argv) std::cout << "Dinit version " << DINIT_VERSION << ".\n"; return 0; } + else if (strcmp(argv[i], "--exit") == 0 || strcmp(argv[i], "-e") == 0) { + exit_after = true; + } + else if (strcmp(argv[i], "--env") == 0 || strcmp(argv[i], "-E") == 0) { + use_environ = true; + } else if (strcmp(argv[i], "--system") == 0 || strcmp(argv[i], "-s") == 0) { user_dinit = false; } @@ -102,7 +116,23 @@ int dinit_monitor_main(int argc, char **argv) } str_failed = argv[i]; } - else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--command")) { + else if (strcmp(argv[i], "--str-set") == 0) { + ++i; + if (i == argc) { + std::cerr << "dinit-monitor: --str-set should be followed by an argument\n"; + return 1; + } + str_set = argv[i]; + } + else if (strcmp(argv[i], "--str-unset") == 0) { + ++i; + if (i == argc) { + std::cerr << "dinit-monitor: --str-unset should be followed by an argument\n"; + return 1; + } + str_unset = argv[i]; + } + else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--command") == 0) { ++i; if (i == argc) { std::cerr << "dinit-monitor: --command/-c should be followed by command\n"; @@ -120,10 +150,12 @@ int dinit_monitor_main(int argc, char **argv) std::cout << "dinit-monitor: monitor Dinit services\n" "\n" "Usage:\n" - " dinit-monitor [options] \n" + " dinit-monitor [options] \n" "\n" "Options:\n" " --help : show this help\n" + " -e, --exit : exit after the first issued command\n" + " -E, --env : monitor activation environment changes\n" " -s, --system : monitor system daemon (default if run as root)\n" " -u, --user : monitor user daemon\n" " -i, --initial : also execute command for initial service state\n" @@ -140,7 +172,7 @@ int dinit_monitor_main(int argc, char **argv) return 1; } - if (services.empty()) { + if (services.empty() && !use_environ) { std::cerr << "dinit-monitor: specify at least one service name\n"; return 1; } @@ -186,12 +218,20 @@ int dinit_monitor_main(int argc, char **argv) // Load all services std::unordered_map service_map; + std::unordered_set environ_set; std::vector> service_init_state; + std::string env_value; for (const char *service_name : services) { handle_t hndl; service_state_t state; + + if (use_environ) { + environ_set.emplace(service_name); + continue; + } + if (!load_service(socknum, rbuffer, service_name, &hndl, &state)) { std::cerr << "dinit-monitor: cannot load service: " << service_name << "\n"; return 1; @@ -201,14 +241,35 @@ int dinit_monitor_main(int argc, char **argv) service_init_state.push_back(std::make_pair(service_name, state)); } - // Issue initial status commands if requested - if (issue_init) { + if (use_environ) { + // Request listening on environ events + request_environ(socknum, rbuffer); + if (issue_init) { + // Get the whole block and see if it's already set + auto envsz = get_allenv(socknum, rbuffer); + while (envsz > 0) { + bool issued; + envsz = read_and_issue(socknum, rbuffer, envsz, environ_set, env_value, command_parts, issued, str_set, str_unset); + if (issued && exit_after) { + return 0; + } + } + } + } + else if (issue_init) { + // Issue initial status commands if requested for (auto state : service_init_state ) { if (state.second == service_state_t::STARTED) { - issue_command(state.first, str_started, command_parts); + issue_command(state.first, nullptr, str_started, command_parts); + if (exit_after) { + return 0; + } } else if (state.second == service_state_t::STOPPED) { - issue_command(state.first, str_stopped, command_parts); + issue_command(state.first, nullptr, str_stopped, command_parts); + if (exit_after) { + return 0; + } } } } @@ -221,7 +282,18 @@ int dinit_monitor_main(int argc, char **argv) int pktlen = (unsigned char) rbuffer[1]; fill_buffer_to(rbuffer, socknum, pktlen); - if (rbuffer[0] == (char)cp_info::SERVICEEVENT) { + if (use_environ && rbuffer[0] == (char)cp_info::ENVEVENT) { + envvar_len_t envln; + rbuffer.extract((char *) &envln, 3, sizeof(envln)); + rbuffer.consume(pktlen); + // this will return 0, we don't want to consume after this + bool issued; + pktlen = read_and_issue(socknum, rbuffer, envln, environ_set, env_value, command_parts, issued, str_set, str_unset); + if (issued && exit_after) { + return 0; + } + } + else if (!use_environ && rbuffer[0] == (char)cp_info::SERVICEEVENT) { handle_t ev_handle; rbuffer.extract((char *) &ev_handle, 2, sizeof(ev_handle)); service_event_t event = static_cast(rbuffer[2 + sizeof(ev_handle)]); @@ -241,7 +313,10 @@ int dinit_monitor_main(int argc, char **argv) else { goto consume_packet; } - issue_command(service_name, event_str, command_parts); + issue_command(service_name, nullptr, event_str, command_parts); + if (exit_after) { + return 0; + } } consume_packet: @@ -298,7 +373,7 @@ int dinit_monitor_main(int argc, char **argv) } -void issue_command(const char* service_name, const char* event_str, std::vector &command_parts) { +static void issue_command(const char* name, const char* value, const char* event, std::vector &command_parts, bool is_env) { std::vector final_cmd_parts; std::vector final_cmd_parts_cstr; @@ -314,10 +389,13 @@ void issue_command(const char* service_name, const char* event_str, std::vector< break; } if (cmd_part.str[i] == 'n') { - cmd_part_str.append(service_name); + cmd_part_str.append(name); + } + else if (cmd_part.str[i] == 'v') { + if (value) cmd_part_str.append(value); } else if (cmd_part.str[i] == 's') { - cmd_part_str.append(event_str); + cmd_part_str.append(event); } else { // invalid specifier, just output as is @@ -480,3 +558,95 @@ static bool load_service(int socknum, cpbuffer_t &rbuffer, const char *name, han return true; } + +static void request_environ(int socknum, cpbuffer_t &rbuffer) +{ + char c = (char)cp_cmd::LISTENENV; + write_all_x(socknum, &c, 1); + + wait_for_reply(rbuffer, socknum); + + cp_rply reply_pkt_h = (cp_rply)rbuffer[0]; + if (reply_pkt_h != cp_rply::ACK) { + throw dinit_protocol_error(); + } + rbuffer.consume(1); +} + +static size_t get_allenv(int socknum, cpbuffer_t &rbuffer) +{ + char buf[2] = { (char)cp_cmd::GETALLENV, 0 }; + write_all_x(socknum, buf, 2); + + wait_for_reply(rbuffer, socknum); + + cp_rply reply_pkt_h = (cp_rply)rbuffer[0]; + if (reply_pkt_h != cp_rply::ALLENV) { + throw dinit_protocol_error(); + } + + // 1-byte packet header, then size_t + constexpr size_t allenv_hdr_size = 1 + sizeof(size_t); + rbuffer.fill_to(socknum, allenv_hdr_size); + + size_t dsize; + rbuffer.extract(&dsize, 1, sizeof(dsize)); + rbuffer.consume(allenv_hdr_size); + + return dsize; +} + +static bool issue_var(std::string &envar, const std::unordered_set &varset, std::vector &command_parts, + const char* str_set, const char* str_unset) +{ + auto eq = envar.find('='); + if (eq == envar.npos) { + /* unset */ + eq = envar.size(); + } + auto *sp = &envar[0]; + sp[eq] = '\0'; + if (varset.empty() || varset.find(sp) != varset.end()) { + if (eq == envar.size()) { + issue_command(sp, nullptr, str_unset, command_parts, true); + } + else { + issue_command(sp, &sp[eq + 1], str_set, command_parts, true); + } + return true; + } + return false; +} + +static size_t read_and_issue(int socknum, cpbuffer_t &rbuffer, size_t dsz, const std::unordered_set &varset, + std::string &enval, std::vector &command_parts, bool &issued, const char* str_set, const char *str_unset) +{ + enval.clear(); + issued = false; + while (dsz > 0) { + auto colen = rbuffer.get_contiguous_length(rbuffer.get_ptr(0)); + auto chlen = std::min((size_t)colen, dsz); + for (unsigned i = 0; i < chlen; ++i) { + if (rbuffer[i] != '\0') { + continue; + } + enval.append(rbuffer.get_ptr(0), rbuffer.get_ptr(0) + i); + rbuffer.consume(i + 1); + issued = issue_var(enval, varset, command_parts, str_set, str_unset); + return dsz - i - 1; + } + // copy what we have so far and fill some more + enval.append(rbuffer.get_ptr(0), rbuffer.get_ptr(0) + chlen); + rbuffer.consume(chlen); + dsz -= chlen; + if (dsz == 0) { + // didn't find null terminator, malformed + throw dinit_protocol_error(); + } + if (rbuffer.get_length() == 0) { + fill_some(rbuffer, socknum); + } + } + // unreachable + throw dinit_protocol_error(); +} diff --git a/src/dinitctl.cc b/src/dinitctl.cc index 56086141..bc47cb71 100644 --- a/src/dinitctl.cc +++ b/src/dinitctl.cc @@ -58,7 +58,7 @@ static int add_remove_dependency(int socknum, cpbuffer_t &rbuffer, bool add, con const char *service_to, dependency_type dep_type, bool verbose); static int enable_disable_service(int socknum, cpbuffer_t &rbuffer, service_dir_opt &service_dir_opts, const char *from, const char *to, bool enable, bool verbose, uint16_t proto_version); -static int do_setenv(int socknum, cpbuffer_t &rbuffer, std::vector &env_names); +static int do_setenv(int socknum, cpbuffer_t &rbuffer, std::vector &env_names, bool unset); static int trigger_service(int socknum, cpbuffer_t &rbuffer, const char *service_name, bool trigger_value); static int cat_service_log(int socknum, cpbuffer_t &rbuffer, const char *service_name, bool do_clear); static int signal_send(int socknum, cpbuffer_t &rbuffer, const char *service_name, sig_num_t sig_num); @@ -82,6 +82,7 @@ enum class ctl_cmd { ENABLE_SERVICE, DISABLE_SERVICE, SETENV, + UNSETENV, SET_TRIGGER, UNSET_TRIGGER, CAT_LOG, @@ -277,6 +278,9 @@ int dinitctl_main(int argc, char **argv) else if (strcmp(argv[i], "setenv") == 0) { command = ctl_cmd::SETENV; } + else if (strcmp(argv[i], "unsetenv") == 0) { + command = ctl_cmd::UNSETENV; + } else if (strcmp(argv[i], "trigger") == 0) { command = ctl_cmd::SET_TRIGGER; } @@ -365,8 +369,8 @@ int dinitctl_main(int argc, char **argv) else if (command == ctl_cmd::ENABLE_SERVICE || command == ctl_cmd::DISABLE_SERVICE) { cmdline_error |= (to_service_name == nullptr); } - else if (command == ctl_cmd::SETENV) { - // Handle SETENV specially, since it needs arguments but they are not service names + else if (command == ctl_cmd::SETENV || command == ctl_cmd::UNSETENV) { + // Handle (UN)SETENV specially, since it needs arguments but they are not service names if (cmd_args.empty()) { cmdline_error = true; } @@ -460,6 +464,7 @@ int dinitctl_main(int argc, char **argv) " dinitctl [options] trigger \n" " dinitctl [options] untrigger \n" " dinitctl [options] setenv [name[=value] ...]\n" + " dinitctl [options] unsetenv [name ...]\n" " dinitctl [options] catlog \n" " dinitctl [options] signal \n" "\n" @@ -591,8 +596,8 @@ int dinitctl_main(int argc, char **argv) return enable_disable_service(socknum, rbuffer, service_dir_opts, service_name, to_service_name, command == ctl_cmd::ENABLE_SERVICE, verbose, daemon_protocol_ver); } - else if (command == ctl_cmd::SETENV) { - return do_setenv(socknum, rbuffer, cmd_args); + else if (command == ctl_cmd::SETENV || command == ctl_cmd::UNSETENV) { + return do_setenv(socknum, rbuffer, cmd_args, command == ctl_cmd::UNSETENV); } else if (command == ctl_cmd::SET_TRIGGER || command == ctl_cmd::UNSET_TRIGGER) { if (daemon_protocol_ver < 2) { @@ -2193,7 +2198,7 @@ static int enable_disable_service(int socknum, cpbuffer_t &rbuffer, service_dir_ return 0; } -static int do_setenv(int socknum, cpbuffer_t &rbuffer, std::vector &env_names) +static int do_setenv(int socknum, cpbuffer_t &rbuffer, std::vector &env_names, bool unset) { using namespace std; @@ -2210,14 +2215,19 @@ static int do_setenv(int socknum, cpbuffer_t &rbuffer, std::vector // either full var or name auto elen = strlen(envp); buf.append(envp, elen); - // if '=' not found, get value from environment - if (!memchr(envp, '=', elen)) { + // if '=' not found, get value from environment, except for unset + auto eq = memchr(envp, '=', elen); + if (!eq && !unset) { buf.push_back('='); auto *envv = getenv(envp); if (envv) { buf.append(envv); } } + else if (eq && unset) { + cerr << "dinitctl: environment variable '" << envp << "' must not contain the '=' sign." << endl; + return 1; + } envvar_len = buf.size() - hdr_len; // sanitize length early on if (buf.size() > cpbuffer_t::get_size()) { diff --git a/src/includes/control-cmds.h b/src/includes/control-cmds.h index bb299ee7..d7be7c45 100644 --- a/src/includes/control-cmds.h +++ b/src/includes/control-cmds.h @@ -11,7 +11,7 @@ // 3 - dinit 0.17.1 (adds QUERYSERVICEDSCDIR) // 4 - dinit 0.18.0 (adds CLOSEHANDLE, GETALLENV) // 5 - (unreleased) (process status now represented as ([int]si_code + [int]si_status) rather than -// a single integer; SERVICEEVENT5 sent alongside SERVICEEVENT) +// a single integer; SERVICEEVENT5 sent alongside SERVICEEVENT; adds LISTENENV, ENVEVENT) // Requests: enum class cp_cmd : dinit_cptypes::cp_cmd_t { @@ -87,6 +87,9 @@ enum class cp_cmd : dinit_cptypes::cp_cmd_t { // Query status of an individual service (5+) SERVICESTATUS5 = 26, + + // Start listening to environment events + LISTENENV = 27, }; // Replies: @@ -175,6 +178,8 @@ enum class cp_info : dinit_cptypes::cp_info_t { SERVICEEVENT = 100, // Service event for protocol version 5+ - 4 byte handle, 1 byte event code, proc_status_t status SERVICEEVENT5 = 101, + // Environment event; 2 bytes length + env string + ENVEVENT = 102, }; #endif diff --git a/src/includes/control.h b/src/includes/control.h index 97ea5840..f78dc02d 100644 --- a/src/includes/control.h +++ b/src/includes/control.h @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -81,7 +82,7 @@ class control_conn_watcher : public eventloop_t::bidi_fd_watcher_impl #include +#include #include #include @@ -62,6 +63,14 @@ struct env_equal_name } }; +// Interface for listening to environment +class env_listener +{ + public: + + virtual void environ_event(environment *env, std::string const &name_and_val, bool overridden) noexcept = 0; +}; + class environment { // Whether to keep the parent environment, as a whole. Individual variables can still be @@ -81,6 +90,8 @@ class environment // set of variables modified or set: env_set set_vars; + std::unordered_set listeners; + string_view find_var_name(string_view var) { const char *var_ch; @@ -90,6 +101,13 @@ class environment return {var.data(), (size_t)(var_ch - var.data())}; } + void notify_listeners(std::string const &var_and_val, bool overridden) + { + for (auto l : listeners) { + l->environ_event(this, var_and_val, overridden); + } + } + public: environment() = default; @@ -253,16 +271,36 @@ class environment return build(env_names()); } - void set_var(std::string &&var_and_val) + void set_var(std::string &&var_and_val, bool notify = false) { string_view var_name = find_var_name(var_and_val); import_from_parent.erase(var_name); - undefine.erase(var_name); + auto n_removed = undefine.erase(var_name); + + bool in_sysenv = false; + if (notify && n_removed == 0) { + // workaround to avoid an allocation; when notify is true, + // we know for sure that the value is sanitized from before + // + // we don't check this when the variable was in undefine, + // as that means we were undefining it explicitly + auto name_size = var_name.size(); + var_and_val[name_size] = '\0'; + in_sysenv = !!bp_sys::getenv(var_and_val.c_str()); + var_and_val[name_size] = '='; + } auto insert_result = set_vars.insert(std::move(var_and_val)); + // if the variable was in sys environment, it is always overridden + bool overridden = in_sysenv; if (!insert_result.second) { *insert_result.first = var_and_val; + overridden = true; + } + + if (notify) { + notify_listeners(*insert_result.first, overridden); } } @@ -275,12 +313,24 @@ class environment } } - void undefine_var(std::string &&var_name) + void undefine_var(std::string &&var_name, bool notify = false) { import_from_parent.erase(var_name); - set_vars.erase(string_view(var_name)); + + auto n_removed = set_vars.erase(string_view(var_name)); + bool was_set = n_removed > 0; + if (notify && !was_set) { + // also track if we're undefining it from system environment + was_set = !!bp_sys::getenv(var_name.c_str()); + } if (keep_parent_env) { - undefine.insert(std::move(var_name)); + auto insert_result = undefine.insert(std::move(var_name)); + if (notify) { + notify_listeners(*insert_result.first, was_set); + } + } + else if (notify) { + notify_listeners(var_name, was_set); } } @@ -291,6 +341,16 @@ class environment undefine.clear(); set_vars.clear(); } + + void add_listener(env_listener * listener) + { + listeners.insert(listener); + } + + void remove_listener(env_listener * listener) noexcept + { + listeners.erase(listener); + } }; // Read and set environment variables from a file. May throw std::bad_alloc, std::system_error.