@@ -93,6 +93,85 @@ std::vector<std::string> hook_environment(const char* event, const HookContext&
9393} // namespace
9494
9595void HookRunner::run (const std::string& command, const char * event, const HookContext& context) {
96+ if (!this ->running_ .empty ()) {
97+ // One at a time: spawned side by side, two events race in the scheduler, and a start
98+ // hook finishing after its own stream's stop hook leaves the amplifier on with the
99+ // player idle. The newest event wins the slot -- the hardware should end in the
100+ // final state, not replay a stale intermediate on the way there.
101+ if (this ->pending_ .has_value ()) {
102+ cli_log (LogLevel::DEBUG , " The queued %s hook was superseded by the %s event" ,
103+ this ->pending_ ->event .c_str (), event);
104+ }
105+ this ->pending_ = PendingHook{command, event, context};
106+ return ;
107+ }
108+ this ->spawn (command, event, context);
109+ }
110+
111+ void HookRunner::poll () {
112+ for (size_t index = 0 ; index < this ->running_ .size ();) {
113+ const RunningHook& hook = this ->running_ [index];
114+ int status = 0 ;
115+ const pid_t reaped = waitpid (hook.pid , &status, WNOHANG );
116+ if (reaped == 0 ) {
117+ ++index;
118+ continue ;
119+ }
120+ if (reaped < 0 && errno == EINTR ) {
121+ // The signal landed on the call, not on the child: the hook is still running, and
122+ // the next poll() asks again. Every handler this daemon installs restarts its call
123+ // -- std::signal()'s BSD semantics for SIGINT and SIGTERM, SA_RESTART for SIGHUP --
124+ // so this is a guard rather than a path taken. Erasing here instead would leak a
125+ // zombie per stream, with nothing in the log to say why.
126+ ++index;
127+ continue ;
128+ }
129+ if (reaped < 0 ) {
130+ // ECHILD is the one that reaches this, and a child that cannot be waited on can
131+ // only be leaked, not re-polled -- so the entry goes. Said out loud because it
132+ // means something else reaped the hook, which is worth a breadcrumb; DEBUG because
133+ // the hook itself ran and there is nothing an operator can do about it.
134+ cli_log (LogLevel::DEBUG , " The %s hook [%d] could not be waited on (%s)" ,
135+ hook.event .c_str (), static_cast <int >(hook.pid ), std::strerror (errno));
136+ }
137+ if (reaped == hook.pid && WIFEXITED (status) && WEXITSTATUS (status) != 0 ) {
138+ cli_log (LogLevel::WARN , " The %s hook [%d] exited %d" , hook.event .c_str (),
139+ static_cast <int >(hook.pid ), WEXITSTATUS (status));
140+ } else if (reaped == hook.pid && WIFSIGNALED (status)) {
141+ cli_log (LogLevel::WARN , " The %s hook [%d] was killed by signal %d" ,
142+ hook.event .c_str (), static_cast <int >(hook.pid ), WTERMSIG (status));
143+ } else if (reaped == hook.pid ) {
144+ cli_log (LogLevel::DEBUG , " The %s hook [%d] finished" , hook.event .c_str (),
145+ static_cast <int >(hook.pid ));
146+ }
147+ this ->running_ .erase (this ->running_ .begin () + static_cast <ptrdiff_t >(index));
148+ }
149+
150+ if (this ->running_ .empty () && this ->pending_ .has_value ()) {
151+ // Its turn: everything ahead of it has been reaped. Taken out of the slot before the
152+ // spawn, which fills running_ again.
153+ const PendingHook next = *this ->pending_ ;
154+ this ->pending_ .reset ();
155+ this ->spawn (next.command , next.event .c_str (), next.context );
156+ }
157+ }
158+
159+ void HookRunner::flush () {
160+ if (!this ->pending_ .has_value ()) {
161+ return ;
162+ }
163+ if (!this ->running_ .empty ()) {
164+ // Ordering is knowingly given up, and said at WARN because it means a hook has hung:
165+ // the pending hook -- the stop the daemon promised on the way out -- must run at all.
166+ cli_log (LogLevel::WARN , " The %s hook has not finished -- running the %s hook beside it" ,
167+ this ->running_ .front ().event .c_str (), this ->pending_ ->event .c_str ());
168+ }
169+ const PendingHook next = *this ->pending_ ;
170+ this ->pending_ .reset ();
171+ this ->spawn (next.command , next.event .c_str (), next.context );
172+ }
173+
174+ void HookRunner::spawn (const std::string& command, const char * event, const HookContext& context) {
96175 // Built before the fork, because after it almost nothing is legal: this process has the
97176 // library's background threads, so the child may only call async-signal-safe functions
98177 // between fork() and execve() -- which allocation, and so std::string, is not.
@@ -157,44 +236,4 @@ void HookRunner::run(const std::string& command, const char* event, const HookCo
157236 this ->running_ .push_back ({pid, event});
158237}
159238
160- void HookRunner::poll () {
161- for (size_t index = 0 ; index < this ->running_ .size ();) {
162- const RunningHook& hook = this ->running_ [index];
163- int status = 0 ;
164- const pid_t reaped = waitpid (hook.pid , &status, WNOHANG );
165- if (reaped == 0 ) {
166- ++index;
167- continue ;
168- }
169- if (reaped < 0 && errno == EINTR ) {
170- // The signal landed on the call, not on the child: the hook is still running, and
171- // the next poll() asks again. Every handler this daemon installs restarts its call
172- // -- std::signal()'s BSD semantics for SIGINT and SIGTERM, SA_RESTART for SIGHUP --
173- // so this is a guard rather than a path taken. Erasing here instead would leak a
174- // zombie per stream, with nothing in the log to say why.
175- ++index;
176- continue ;
177- }
178- if (reaped < 0 ) {
179- // ECHILD is the one that reaches this, and a child that cannot be waited on can
180- // only be leaked, not re-polled -- so the entry goes. Said out loud because it
181- // means something else reaped the hook, which is worth a breadcrumb; DEBUG because
182- // the hook itself ran and there is nothing an operator can do about it.
183- cli_log (LogLevel::DEBUG , " The %s hook [%d] could not be waited on (%s)" ,
184- hook.event .c_str (), static_cast <int >(hook.pid ), std::strerror (errno));
185- }
186- if (reaped == hook.pid && WIFEXITED (status) && WEXITSTATUS (status) != 0 ) {
187- cli_log (LogLevel::WARN , " The %s hook [%d] exited %d" , hook.event .c_str (),
188- static_cast <int >(hook.pid ), WEXITSTATUS (status));
189- } else if (reaped == hook.pid && WIFSIGNALED (status)) {
190- cli_log (LogLevel::WARN , " The %s hook [%d] was killed by signal %d" ,
191- hook.event .c_str (), static_cast <int >(hook.pid ), WTERMSIG (status));
192- } else if (reaped == hook.pid ) {
193- cli_log (LogLevel::DEBUG , " The %s hook [%d] finished" , hook.event .c_str (),
194- static_cast <int >(hook.pid ));
195- }
196- this ->running_ .erase (this ->running_ .begin () + static_cast <ptrdiff_t >(index));
197- }
198- }
199-
200239} // namespace sendspin_cli
0 commit comments