Skip to content

Commit f035d32

Browse files
committed
[ACTP] restore Windows shutdown and logging in par-control main
This layer rewrites the binary entry point and lost two Windows behaviors from the lifecycle layer. CTRL_BREAK: dd-procmgrd stops children with GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT), but shutdown_signal only listened for Ctrl-C off Unix. par-control would therefore ignore its stop signal entirely, sit until the 180s stop_timeout expired, and be killed with the job object - abandoning in-flight actions instead of draining and publishing their outcomes, and stalling every Agent stop or upgrade on Windows by three minutes. Log file: back to None here, which means no log at all on Windows, since procmgrd's `stdout: inherit` becomes the null device when it runs as a service with no inheritable handles. Restores the program-data-root log path (registry ConfigRoot, else %ProgramData%\Datadog), so the location does not depend on where --config points. Also returns ExitCode and logs the failure instead of returning Err, so a startup failure is visible in the log rather than only on a stderr that goes nowhere, and uses the platform-correct --config default. Finally, drops this layer's copy of the fake process-manager harness in favor of the shared test_support module introduced in the lifecycle layer, so the same in-process daemon is defined once for the whole stack.
1 parent cd96621 commit f035d32

2 files changed

Lines changed: 77 additions & 153 deletions

File tree

pkg/privateactionrunner/par-control/src/bins/par-control.rs

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ use par_control::executor::ExecutorDispatcher;
1616
use par_control::jwt::{Es256Signer, JwtSigner};
1717
use par_control::opms::{HttpOpms, HttpOpmsConfig};
1818
use par_control::orchestrator::{Orchestrator, Params};
19+
use par_control::platform;
1920
use par_control::procmgr::ProcmgrLifecycle;
2021
use std::path::PathBuf;
22+
use std::process::ExitCode;
2123
use std::sync::Arc;
2224

2325
#[derive(Parser)]
2426
#[command(name = "par-control", about = "Private Action Runner control plane")]
2527
struct Cli {
26-
#[arg(short = 'c', long, default_value = "/etc/datadog-agent/datadog.yaml")]
28+
#[arg(short = 'c', long, default_value = platform::default_config_path())]
2729
config: PathBuf,
2830

2931
/// Existing Go Private Action Runner binary used to resolve the Agent's
@@ -38,15 +40,29 @@ struct Cli {
3840
}
3941

4042
#[tokio::main]
41-
async fn main() -> Result<()> {
43+
async fn main() -> ExitCode {
44+
// Report failures through the logger rather than by returning Err: anyhow
45+
// prints to stderr, which dd-procmgrd sends to the null device when it runs
46+
// as a Windows service, so a startup failure would leave no trace at all.
47+
match run().await {
48+
Ok(()) => ExitCode::SUCCESS,
49+
Err(error) => {
50+
log::error!("par-control failed: {error:#}");
51+
log::logger().flush();
52+
ExitCode::FAILURE
53+
}
54+
}
55+
}
56+
57+
async fn run() -> Result<()> {
4258
let cli = Cli::parse();
4359

4460
// Initialize logging before the launch gate so clean exits and config errors
4561
// are visible. Logging failure does not prevent the runner from starting.
4662
if let Err(e) = dd_agent_log::init(dd_agent_log::LogConfig {
4763
logger_name: "PAR-CONTROL",
4864
level: log_level_from_yaml_file(&cli.config),
49-
log_file: None,
65+
log_file: platform::default_log_file(),
5066
}) {
5167
eprintln!("par-control: could not initialize the logger: {e}");
5268
}
@@ -115,25 +131,42 @@ async fn main() -> Result<()> {
115131
Ok(())
116132
}
117133

118-
/// Resolves when the process receives Ctrl-C or (on Unix) SIGTERM.
134+
/// Resolves when dd-procmgrd asks par-control to stop: SIGTERM on Unix,
135+
/// CTRL_BREAK on Windows. Ctrl-C additionally covers interactive runs.
136+
#[cfg(unix)]
119137
async fn shutdown_signal() {
120-
#[cfg(unix)]
121-
{
122-
use tokio::signal::unix::{SignalKind, signal};
123-
let mut term = match signal(SignalKind::terminate()) {
124-
Ok(s) => s,
125-
Err(_) => {
126-
let _ = tokio::signal::ctrl_c().await;
127-
return;
128-
}
129-
};
130-
tokio::select! {
131-
_ = tokio::signal::ctrl_c() => {},
132-
_ = term.recv() => {},
138+
use tokio::signal::unix::{SignalKind, signal};
139+
let mut term = match signal(SignalKind::terminate()) {
140+
Ok(s) => s,
141+
Err(_) => {
142+
let _ = tokio::signal::ctrl_c().await;
143+
return;
133144
}
145+
};
146+
tokio::select! {
147+
_ = tokio::signal::ctrl_c() => {},
148+
_ = term.recv() => {},
134149
}
135-
#[cfg(not(unix))]
136-
{
137-
let _ = tokio::signal::ctrl_c().await;
150+
}
151+
152+
/// dd-procmgrd stops children with `GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT)`
153+
/// (`send_graceful_stop` in `pkg/procmgr/rust/src/platform/windows.rs`), so
154+
/// CTRL_BREAK is the event that matters in production; CTRL_C only covers
155+
/// interactive runs. Missing CTRL_BREAK would mean never draining: par-control
156+
/// would sit until `stop_timeout` expired and then be killed with the job
157+
/// object, abandoning in-flight actions instead of publishing their outcomes.
158+
#[cfg(windows)]
159+
async fn shutdown_signal() {
160+
match tokio::signal::windows::ctrl_break() {
161+
Ok(mut ctrl_break) => {
162+
tokio::select! {
163+
_ = tokio::signal::ctrl_c() => {},
164+
_ = ctrl_break.recv() => {},
165+
}
166+
}
167+
Err(error) => {
168+
log::warn!("could not listen for CTRL_BREAK, falling back to CTRL_C: {error}");
169+
let _ = tokio::signal::ctrl_c().await;
170+
}
138171
}
139172
}

pkg/privateactionrunner/par-control/src/procmgr.rs

Lines changed: 24 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -185,111 +185,15 @@ impl ExecutorLifecycle for ProcmgrLifecycle {
185185
#[cfg(test)]
186186
mod tests {
187187
use super::*;
188-
use procmgr::process_manager_server::{ProcessManager, ProcessManagerServer};
189-
use std::sync::{Arc, Mutex};
190-
use tonic::{Request, Response, Status};
188+
use crate::test_support::{FakeProcmgr, serve_procmgr};
189+
use std::sync::Arc;
190+
use tonic::Status;
191191

192192
const TEST_PROCESS_NAME: &str = "datadog-agent-action-executor";
193193

194-
#[derive(Default)]
195-
struct FakeProcmgr {
196-
state: Mutex<Option<i32>>,
197-
start_result: Mutex<Option<Status>>,
198-
starts: Mutex<u32>,
199-
stops: Mutex<u32>,
200-
hang: bool,
201-
}
202-
203-
/// Newtype so the trait impl has a local self type: under Bazel the generated
204-
/// bindings live in a foreign crate, so implementing a foreign trait for
205-
/// `Arc<FakeProcmgr>` would break the orphan rule even though it compiles
206-
/// under `cargo`, where `include_proto!` generates the trait locally.
207-
#[derive(Clone)]
208-
struct FakeService(Arc<FakeProcmgr>);
209-
210-
#[tonic::async_trait]
211-
impl ProcessManager for FakeService {
212-
async fn describe(
213-
&self,
214-
_: Request<procmgr::DescribeRequest>,
215-
) -> Result<Response<procmgr::DescribeResponse>, Status> {
216-
if self.0.hang {
217-
tokio::time::sleep(Duration::from_secs(3600)).await;
218-
}
219-
let state = *self.0.state.lock().unwrap();
220-
Ok(Response::new(procmgr::DescribeResponse {
221-
detail: state.map(|state| procmgr::ProcessDetail {
222-
name: TEST_PROCESS_NAME.to_string(),
223-
state,
224-
..Default::default()
225-
}),
226-
}))
227-
}
228-
229-
async fn start(
230-
&self,
231-
_: Request<procmgr::StartRequest>,
232-
) -> Result<Response<procmgr::StartResponse>, Status> {
233-
*self.0.starts.lock().unwrap() += 1;
234-
if let Some(status) = self.0.start_result.lock().unwrap().clone() {
235-
return Err(status);
236-
}
237-
Ok(Response::new(procmgr::StartResponse::default()))
238-
}
239-
240-
async fn stop(
241-
&self,
242-
_: Request<procmgr::StopRequest>,
243-
) -> Result<Response<procmgr::StopResponse>, Status> {
244-
*self.0.stops.lock().unwrap() += 1;
245-
Ok(Response::new(procmgr::StopResponse::default()))
246-
}
247-
248-
async fn list(
249-
&self,
250-
_: Request<procmgr::ListRequest>,
251-
) -> Result<Response<procmgr::ListResponse>, Status> {
252-
Err(Status::unimplemented("list"))
253-
}
254-
async fn get_status(
255-
&self,
256-
_: Request<procmgr::GetStatusRequest>,
257-
) -> Result<Response<procmgr::GetStatusResponse>, Status> {
258-
Err(Status::unimplemented("get_status"))
259-
}
260-
async fn create(
261-
&self,
262-
_: Request<procmgr::CreateRequest>,
263-
) -> Result<Response<procmgr::CreateResponse>, Status> {
264-
Err(Status::unimplemented("create"))
265-
}
266-
async fn reload_config(
267-
&self,
268-
_: Request<procmgr::ReloadConfigRequest>,
269-
) -> Result<Response<procmgr::ReloadConfigResponse>, Status> {
270-
Err(Status::unimplemented("reload_config"))
271-
}
272-
async fn get_config(
273-
&self,
274-
_: Request<procmgr::GetConfigRequest>,
275-
) -> Result<Response<procmgr::GetConfigResponse>, Status> {
276-
Err(Status::unimplemented("get_config"))
277-
}
278-
}
279-
280194
#[cfg(unix)]
281-
async fn serve(fake: Arc<FakeProcmgr>) -> (ProcmgrLifecycle, tempfile::TempDir) {
282-
use tokio_stream::wrappers::UnixListenerStream;
283-
284-
let dir = tempfile::tempdir().unwrap();
285-
let socket = dir.path().join("dd-procmgrd.sock");
286-
let listener = tokio::net::UnixListener::bind(&socket).unwrap();
287-
tokio::spawn(async move {
288-
let _ = tonic::transport::Server::builder()
289-
.add_service(ProcessManagerServer::new(FakeService(fake)))
290-
.serve_with_incoming(UnixListenerStream::new(listener))
291-
.await;
292-
});
195+
async fn lifecycle_for(fake: Arc<FakeProcmgr>) -> (ProcmgrLifecycle, tempfile::TempDir) {
196+
let (socket, dir) = serve_procmgr(fake).await;
293197
(
294198
ProcmgrLifecycle::new(&socket, TEST_PROCESS_NAME.to_string()),
295199
dir,
@@ -308,19 +212,15 @@ mod tests {
308212
procmgr::ProcessState::Running,
309213
procmgr::ProcessState::Stopping,
310214
] {
311-
let fake = Arc::new(FakeProcmgr {
312-
state: Mutex::new(Some(state as i32)),
313-
..Default::default()
314-
});
315-
let (lifecycle, _dir) = serve(Arc::clone(&fake)).await;
215+
let fake = FakeProcmgr::in_state(state);
216+
let (lifecycle, _dir) = lifecycle_for(Arc::clone(&fake)).await;
316217

317218
lifecycle
318219
.ensure_started()
319220
.await
320221
.unwrap_or_else(|e| panic!("state {state:?} should be adopted: {e:#}"));
321-
assert_eq!(
322-
*fake.starts.lock().unwrap(),
323-
0,
222+
assert!(
223+
fake.started().is_empty(),
324224
"state {state:?} is alive; Start must not be issued"
325225
);
326226
}
@@ -330,29 +230,26 @@ mod tests {
330230
#[cfg(unix)]
331231
#[tokio::test]
332232
async fn ensure_started_tolerates_a_start_race() {
333-
let fake = Arc::new(FakeProcmgr {
334-
state: Mutex::new(Some(procmgr::ProcessState::Exited as i32)),
335-
start_result: Mutex::new(Some(Status::failed_precondition("already running"))),
336-
..Default::default()
337-
});
338-
let (lifecycle, _dir) = serve(Arc::clone(&fake)).await;
233+
let fake = FakeProcmgr::failing_start(
234+
procmgr::ProcessState::Exited,
235+
Status::failed_precondition("already running"),
236+
);
237+
let (lifecycle, _dir) = lifecycle_for(Arc::clone(&fake)).await;
339238

340239
lifecycle.ensure_started().await.expect("race is not fatal");
341-
assert_eq!(*fake.starts.lock().unwrap(), 1);
240+
assert_eq!(fake.started().len(), 1);
342241
}
343242

344243
#[cfg(unix)]
345244
#[tokio::test]
346245
async fn ensure_started_propagates_other_failures() {
347-
let fake = Arc::new(FakeProcmgr {
348-
state: Mutex::new(Some(procmgr::ProcessState::Exited as i32)),
349-
start_result: Mutex::new(Some(Status::not_found("no such process"))),
350-
..Default::default()
351-
});
352-
let (lifecycle, _dir) = serve(fake).await;
246+
let fake = FakeProcmgr::failing_start(
247+
procmgr::ProcessState::Exited,
248+
Status::not_found("no such process"),
249+
);
250+
let (lifecycle, _dir) = lifecycle_for(fake).await;
353251

354-
let error = lifecycle.ensure_started().await.unwrap_err();
355-
let rendered = format!("{error:#}");
252+
let rendered = format!("{:#}", lifecycle.ensure_started().await.unwrap_err());
356253
assert!(rendered.contains("Start failed"), "{rendered}");
357254
assert!(rendered.contains("no such process"), "{rendered}");
358255
}
@@ -363,24 +260,18 @@ mod tests {
363260
#[cfg(unix)]
364261
#[tokio::test]
365262
async fn rpcs_time_out_against_an_unresponsive_daemon() {
366-
let fake = Arc::new(FakeProcmgr {
367-
hang: true,
368-
..Default::default()
369-
});
370-
let (mut lifecycle, _dir) = serve(fake).await;
263+
let (mut lifecycle, _dir) = lifecycle_for(FakeProcmgr::unresponsive()).await;
371264
lifecycle.rpc_timeout = Duration::from_millis(50);
372265

373-
let error = lifecycle.is_running().await.unwrap_err();
374-
let rendered = format!("{error:#}");
266+
let rendered = format!("{:#}", lifecycle.is_running().await.unwrap_err());
375267
assert!(rendered.contains("did not respond within"), "{rendered}");
376268
}
377269

378270
/// A missing definition means there is nothing to reap or report on.
379271
#[cfg(unix)]
380272
#[tokio::test]
381273
async fn reports_a_vanished_process_as_exited() {
382-
let fake = Arc::new(FakeProcmgr::default());
383-
let (lifecycle, _dir) = serve(fake).await;
274+
let (lifecycle, _dir) = lifecycle_for(FakeProcmgr::vanished()).await;
384275

385276
assert!(lifecycle.has_exited().await.unwrap());
386277
assert!(!lifecycle.is_running().await.unwrap());

0 commit comments

Comments
 (0)