Skip to content

Commit b004a03

Browse files
authored
feat: windows service installation [NR-485539] (#1880)
1 parent 870fcb4 commit b004a03

File tree

9 files changed

+284
-4
lines changed

9 files changed

+284
-4
lines changed

.goreleaser.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ archives:
9797
builds:
9898
- newrelic-agent-control-windows
9999
- newrelic-agent-control-cli-windows
100+
files:
101+
- src: build/package/windows/install.ps1
102+
dst: install.ps1
103+
- src: build/package/windows/uninstall.ps1
104+
dst: uninstall.ps1
100105

101106
nfpms: # deb and rpm are only built for linux targets
102107
- package_name: newrelic-agent-control

Cargo.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

THIRD_PARTY_NOTICES.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2740,6 +2740,13 @@ Distributed under the following license(s):
27402740

27412741
* CDLA-Permissive-2.0
27422742

2743+
## widestring <https://crates.io/crates/widestring>
2744+
2745+
Distributed under the following license(s):
2746+
2747+
* MIT
2748+
* Apache-2.0
2749+
27432750
## windows <https://crates.io/crates/windows>
27442751

27452752
Distributed under the following license(s):
@@ -2803,6 +2810,13 @@ Distributed under the following license(s):
28032810
* MIT
28042811
* Apache-2.0
28052812

2813+
## windows-service <https://crates.io/crates/windows-service>
2814+
2815+
Distributed under the following license(s):
2816+
2817+
* MIT
2818+
* Apache-2.0
2819+
28062820
## windows-strings <https://crates.io/crates/windows-strings>
28072821

28082822
Distributed under the following license(s):

agent-control/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ windows = { workspace = true, features = [
8484
"Win32_System_Console",
8585
"Win32_System_Threading",
8686
] }
87+
windows-service = "0.8.0"
8788

8889
[target.'cfg(target_os = "windows")'.dependencies]
8990
windows-sys = { workspace = true }

agent-control/src/bin/main_onhost.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
//! It implements the basic functionality of parsing the command line arguments and either
44
//! performing one-shot actions or starting the main agent control process.
55
#![warn(missing_docs)]
6-
76
use newrelic_agent_control::agent_control::run::on_host::AGENT_CONTROL_MODE_ON_HOST;
87
use newrelic_agent_control::agent_control::run::{AgentControlRunConfig, AgentControlRunner};
98
use newrelic_agent_control::command::Command;
@@ -16,8 +15,38 @@ use std::error::Error;
1615
use std::process::ExitCode;
1716
use tracing::{error, info, trace};
1817

18+
#[cfg(target_os = "windows")]
19+
use newrelic_agent_control::command::windows::{WINDOWS_SERVICE_NAME, setup_windows_service};
20+
21+
#[cfg(target_os = "windows")]
22+
windows_service::define_windows_service!(ffi_service_main, service_main);
23+
1924
fn main() -> ExitCode {
20-
Command::run(AGENT_CONTROL_MODE_ON_HOST, _main)
25+
#[cfg(target_family = "unix")]
26+
{
27+
Command::run(AGENT_CONTROL_MODE_ON_HOST, _main)
28+
}
29+
30+
#[cfg(target_os = "windows")]
31+
{
32+
if windows_service::service_dispatcher::start(WINDOWS_SERVICE_NAME, ffi_service_main)
33+
.is_err()
34+
{
35+
// Not running as Windows Service, run normally
36+
return Command::run(AGENT_CONTROL_MODE_ON_HOST, |cfg, tracer| {
37+
_main(cfg, tracer, false)
38+
});
39+
}
40+
ExitCode::SUCCESS
41+
}
42+
}
43+
44+
#[cfg(target_os = "windows")]
45+
/// Entry-point for Windows Service
46+
fn service_main(_arguments: Vec<std::ffi::OsString>) {
47+
let _ = Command::run(AGENT_CONTROL_MODE_ON_HOST, |cfg, tracer| {
48+
_main(cfg, tracer, true)
49+
});
2150
}
2251

2352
/// This is the actual main function.
@@ -33,6 +62,7 @@ fn main() -> ExitCode {
3362
fn _main(
3463
agent_control_run_config: AgentControlRunConfig,
3564
_tracer: Vec<TracingGuardBox>, // Needs to take ownership of the tracer as it can be shutdown on drop
65+
#[cfg(target_os = "windows")] as_windows_service: bool,
3666
) -> Result<(), Box<dyn Error>> {
3767
#[cfg(not(feature = "disable-asroot"))]
3868
if !is_elevated()? {
@@ -52,12 +82,22 @@ fn _main(
5282
let (application_event_publisher, application_event_consumer) = pub_sub();
5383

5484
trace!("creating the signal handler");
55-
create_shutdown_signal_handler(application_event_publisher)?;
85+
create_shutdown_signal_handler(application_event_publisher.clone())?;
86+
87+
#[cfg(target_os = "windows")]
88+
let tear_down_windows_service = as_windows_service
89+
.then(|| setup_windows_service(application_event_publisher))
90+
.transpose()?;
5691

5792
// Create the actual agent control runner with the rest of required configs and the application_event_consumer
5893
AgentControlRunner::new(agent_control_run_config, application_event_consumer)?.run()?;
5994

60-
info!("exiting gracefully");
95+
#[cfg(target_os = "windows")]
96+
if let Some(tear_down_fn) = tear_down_windows_service {
97+
tear_down_fn()?;
98+
}
99+
100+
info!("Exiting gracefully");
61101

62102
Ok(())
63103
}

agent-control/src/command.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ use std::process::ExitCode;
2525
use std::sync::Arc;
2626
use tracing::{error, info};
2727

28+
#[cfg(target_os = "windows")]
29+
pub mod windows;
30+
2831
/// All possible errors that can happen while running the initialization.
2932
#[derive(Debug, thiserror::Error)]
3033
pub enum InitError {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//! This module contains functions to handle the Windows version of the main, which involves a Windows Service
2+
//! running mode.
3+
4+
use crate::event::ApplicationEvent;
5+
use crate::event::channel::EventPublisher;
6+
use std::error::Error;
7+
use tracing::error;
8+
use windows_service::{
9+
service::{
10+
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus,
11+
ServiceType,
12+
},
13+
service_control_handler::{self, ServiceControlHandlerResult},
14+
};
15+
16+
/// Defines the name for the Windows Service.
17+
pub const WINDOWS_SERVICE_NAME: &str = "newrelic-agent-control";
18+
19+
/// Type alias to simplify [setup_windows_service] definition.
20+
type WinServiceResult = Result<(), Box<dyn Error>>;
21+
22+
/// Sets up the Windows Service by creating the status handler and setting the service status as [WindowsServiceStatus::Running].
23+
/// It returns a function to tear the service down when the Agent Control finishes its execution.
24+
pub fn setup_windows_service(
25+
application_event_publisher: EventPublisher<ApplicationEvent>,
26+
) -> Result<impl Fn() -> WinServiceResult, Box<dyn Error>> {
27+
let windows_status_handler = service_control_handler::register(
28+
WINDOWS_SERVICE_NAME,
29+
windows_event_handler(application_event_publisher),
30+
)?;
31+
windows_status_handler.set_service_status(WindowsServiceStatus::Running.into())?;
32+
33+
Ok(move || {
34+
// TODO: check if we should inform of stop-requested in case the graceful shutdown takes too long.
35+
windows_status_handler.set_service_status(WindowsServiceStatus::Stopped.into())?;
36+
Ok(())
37+
})
38+
}
39+
40+
/// Handles windows services events and stops the Agent Control if the specific events are received.
41+
/// See the '[Service Control Handler Function](https://learn.microsoft.com/en-us/windows/win32/services/service-control-handler-function)'
42+
/// page for details.
43+
pub fn windows_event_handler(
44+
publisher: EventPublisher<ApplicationEvent>,
45+
) -> impl Fn(ServiceControl) -> ServiceControlHandlerResult {
46+
move |event: ServiceControl| -> ServiceControlHandlerResult {
47+
match event {
48+
ServiceControl::Stop => {
49+
let _ = publisher
50+
.publish(ApplicationEvent::StopRequested)
51+
.inspect_err(|err| error!("Could not send agent control stop request {err}"));
52+
ServiceControlHandlerResult::NoError
53+
}
54+
// Interrogate needs to return `NoError` even if it is a No-Op operation.
55+
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
56+
_ => ServiceControlHandlerResult::NotImplemented,
57+
}
58+
}
59+
}
60+
61+
/// Internal, simplified representation of [ServiceStatus]
62+
pub enum WindowsServiceStatus {
63+
/// Represents that the service is running
64+
Running,
65+
/// Represents that the service is stopped
66+
Stopped,
67+
}
68+
69+
impl From<WindowsServiceStatus> for ServiceStatus {
70+
fn from(value: WindowsServiceStatus) -> Self {
71+
let (current_state, controls_accepted) = match value {
72+
WindowsServiceStatus::Running => (ServiceState::Running, ServiceControlAccept::STOP),
73+
WindowsServiceStatus::Stopped => (ServiceState::Stopped, ServiceControlAccept::empty()),
74+
};
75+
ServiceStatus {
76+
service_type: ServiceType::OWN_PROCESS,
77+
current_state,
78+
controls_accepted,
79+
exit_code: ServiceExitCode::Win32(0),
80+
checkpoint: 0,
81+
wait_hint: std::time::Duration::default(),
82+
process_id: None,
83+
}
84+
}
85+
}

build/package/windows/install.ps1

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# PowerShell script to install New Relic Agent Control as a Windows Service
2+
# Run this script with Administrator privileges
3+
4+
param(
5+
[Parameter(Mandatory=$false)]
6+
[switch]$ServiceOverwrite = $false
7+
)
8+
9+
# Check for administrator privileges
10+
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
11+
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
12+
Write-Error "Admin permission is required. Please, open a Windows PowerShell session with administrative rights.";
13+
exit 1
14+
}
15+
16+
$serviceName = "newrelic-agent-control"
17+
$serviceDisplayName = "New Relic Agent Control"
18+
19+
$acDir = [IO.Path]::Combine($env:ProgramFiles, 'New Relic\newrelic-agent-control')
20+
$acLocalConfigDir = [IO.Path]::Combine($acDir, 'local-data\agent-control')
21+
$acExecPath = [IO.Path]::Combine($acDir, 'newrelic-agent-control.exe')
22+
23+
$acDataDir = [IO.Path]::Combine($env:ProgramData, 'New Relic\newrelic-agent-control')
24+
$acLogsDir = [IO.Path]::Combine($acDataDir, 'logs')
25+
26+
# If the service already exists and overwriting is allowed, the service is stopped and removed.
27+
$existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
28+
if ($existingService) {
29+
if ($ServiceOverwrite -eq $false)
30+
{
31+
"service $serviceName already exists. Use flag '-ServiceOverwrite' to update it"
32+
exit 1
33+
}
34+
Write-Host "Service '$serviceName' already exists. Stopping and removing..."
35+
Stop-Service $serviceName | Out-Null
36+
37+
$serviceToRemove = Get-WmiObject -Class Win32_Service -Filter "name='$serviceName'"
38+
if ($serviceToRemove)
39+
{
40+
$serviceToRemove.delete() | Out-Null
41+
}
42+
}
43+
44+
$versionData = (& ".\newrelic-agent-control.exe" --version) -replace ',.*$', ''
45+
Write-Host "Installing $versionData"
46+
47+
Write-Host "Creating New Relic Agent Control directories..."
48+
49+
[System.IO.Directory]::CreateDirectory("$acDir") | Out-Null
50+
[System.IO.Directory]::CreateDirectory("$acDataDir") | Out-Null
51+
[System.IO.Directory]::CreateDirectory("$acLogsDir") | Out-Null
52+
[System.IO.Directory]::CreateDirectory("$acLocalConfigDir") | Out-Null
53+
54+
Write-Host "Copying New Relic Agent Control program files..."
55+
Copy-Item -Path ".\newrelic-agent-control.exe" -Destination "$acDir"
56+
57+
# Generate configuration
58+
# TODO: make this configurable through ps1 arguments (identity related args, region, ...)
59+
& ".\newrelic-agent-control-cli.exe" generate-config --fleet-disabled --region us --agent-set no-agents --output-path "`"$acLocalConfigDir\local_config.yaml`""
60+
61+
# Install the service
62+
Write-Host "Installing New Relic Agent Control service..."
63+
New-Service -Name $serviceName -DisplayName "$serviceDisplayName" -BinaryPathName "$acExecPath" -StartupType Automatic | Out-Null
64+
if ($?)
65+
{
66+
Start-Service -Name $serviceName | Out-Null
67+
Write-Host "Installation completed!"
68+
} else {
69+
Write-Host "Error creating service $serviceName"
70+
exit 1
71+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# PowerShell script to uninstall the New Relic Agent Control Windows Service
2+
# Run this script with Administrator privileges
3+
4+
# Check for administrator privileges
5+
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
6+
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
7+
Write-Error "Admin permission is required. Please, open a Windows PowerShell session with administrative rights.";
8+
exit 1
9+
}
10+
11+
$serviceName = "newrelic-agent-control"
12+
$acDir = [IO.Path]::Combine($env:ProgramFiles, 'New Relic\newrelic-agent-control')
13+
$acExecPath = [IO.Path]::Combine($acDir, 'newrelic-agent-control.exe')
14+
15+
# Stop and remove the service if exists
16+
$existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
17+
if ($existingService) {
18+
Write-Host "Stopping and removing $serviceName..."
19+
Stop-Service $serviceName | Out-Null
20+
21+
$serviceToRemove = Get-WmiObject -Class Win32_Service -Filter "name='$serviceName'"
22+
if ($serviceToRemove)
23+
{
24+
$serviceToRemove.delete() | Out-Null
25+
}
26+
}
27+
28+
# Remove the executable if exists
29+
if (Test-Path $acExecPath) {
30+
Write-Host "Deleting $acExecPath..."
31+
Remove-Item -Path $acExecPath -Force
32+
}
33+
34+
Write-Host "Uninstallation completed!"

0 commit comments

Comments
 (0)