Full rebrand across cosmetic branding, code identifiers, and infrastructure/data-plane naming, using the supplied Cairn OBS logo package. Cosmetic: favicon/logo swap (also closes a stale license-audit finding -- the old favicon was SvelteKit's unreplaced scaffold logo), new centered welcome landing page, larger/legible sidebar logo, page titles, CLAUDE.md/README/docs prose. Code identifiers: Go module path github.com/sentry/sentry -> github.com/cairnobs/cairnobs across all 13 modules and ~91 files (protoc regenerated); Rust crates sentry-agent/sentry-parser/sentry-search -> cairnobs-*; CLI sentryctl -> cairnobsctl; Terraform provider fully renamed (sentry_dashboard etc. -> cairnobs_dashboard, provider type, env vars); every session/auth cookie name; agent config paths and Windows service identity. Deliberately preserved: the gRPC wire protocol's protobuf packages (sentry.logs.v1, sentry.agent.v1) and their Go import directory (proto/sentry/...) -- renaming the wire-level package would break every currently-deployed agent binary (confirmed two real hosts, including mail.inbuxa.com, are actively streaming through this exact contract) until rebuilt and redeployed in lockstep with an ingest cutover. Only the Go module path wrapping the generated code changes. Infrastructure: every docker-compose container name (root and three component-level compose files); the Helm chart (directory, Chart.yaml, named-template helpers, all templates, values.yaml image repos); Kubernetes Operator (CRD group sentry.io -> cairnobs.io, both CRD YAML files, Go identifiers, RBAC markers); the coupled enterprise/tenantcrd package. Caught and fixed real path-coupling bugs along the way: the Helm chart's search/ingest volume mounts and the dev-only-credential detection constant vs. docker-compose.yml's literal values had to move together or a security warning would have silently stopped firing. Data plane: Postgres database sentry_metadata -> cairnobs_metadata and role sentry -> cairnobs; ClickHouse database sentry -> cairnobs; Kafka topic sentry.logs.raw -> cairnobs.logs.raw and its consumer groups. Source-level defaults, docker-compose.yml, and every migrate.sh/ provision script default updated together; already-applied migration files left untouched per this repo's immutable-migration convention. Verified at every layer: all 13 Go modules build/vet/test clean, both Rust workspaces (agent, search) build/clippy/test clean, npm run check/ build clean, docker compose config validates on all four compose files. Live-verified against a real docker stack multiple times through this work, including a final fresh-volume run confirming the actual renamed Postgres database/role, ClickHouse database, and Kafka topic all work end to end with a real login and query, zero console errors.
171 lines
6.8 KiB
Rust
171 lines
6.8 KiB
Rust
//! Windows Service Control Manager integration: install/uninstall the
|
|
//! agent as a native Windows service, and the SCM-invoked entry point
|
|
//! that actually runs it as one.
|
|
//!
|
|
//! UNVERIFIED, same caveat as source/windows_eventlog.rs and
|
|
//! source/etw.rs -- written against the `windows-service` crate's
|
|
//! documented usage pattern, not compiled or run (no Windows toolchain
|
|
//! available). This one is lower-risk than etw.rs (no raw FFI struct
|
|
//! layout to get right; `windows-service` wraps that), but the
|
|
//! stop-signal plumbing between the SCM callback and the tokio-running
|
|
//! agent thread is new code worth testing carefully.
|
|
//!
|
|
//! Known limitation, not addressed here: when running as a service (no
|
|
//! console attached), `tracing_subscriber::fmt()`'s stdout writer has
|
|
//! nowhere to go. Logs won't be visible anywhere useful until this is
|
|
//! redirected to a file or an actual Windows Event Log tracing sink is
|
|
//! written -- flagging this now rather than shipping it silently broken.
|
|
|
|
use anyhow::{Context, Result};
|
|
use std::ffi::OsString;
|
|
use std::time::Duration;
|
|
use windows_service::service::{
|
|
ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode,
|
|
ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType,
|
|
};
|
|
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
|
|
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
|
|
use windows_service::{define_windows_service, service_dispatcher};
|
|
|
|
pub const SERVICE_NAME: &str = "CairnObsAgent";
|
|
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
|
|
|
|
/// Registers this binary as a Windows service: Automatic start,
|
|
/// LocalSystem account, invoked with the `run-service` subcommand (which
|
|
/// is what the SCM actually launches — not a bare `cairnobs-agent` with no
|
|
/// arguments). Requires an administrator shell.
|
|
pub fn install() -> Result<()> {
|
|
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CREATE_SERVICE)
|
|
.context("opening Service Control Manager")?;
|
|
|
|
let exe_path = std::env::current_exe().context("resolving current executable path")?;
|
|
|
|
let service_info = ServiceInfo {
|
|
name: OsString::from(SERVICE_NAME),
|
|
display_name: OsString::from("Cairn OBS Log Agent"),
|
|
service_type: SERVICE_TYPE,
|
|
start_type: ServiceStartType::AutoStart,
|
|
error_control: ServiceErrorControl::Normal,
|
|
executable_path: exe_path,
|
|
launch_arguments: vec![OsString::from("run-service")],
|
|
dependencies: vec![],
|
|
account_name: None, // LocalSystem
|
|
account_password: None,
|
|
};
|
|
|
|
let service = manager
|
|
.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)
|
|
.context("creating service")?;
|
|
service
|
|
.set_description("Ships local logs to Cairn OBS ingest over mTLS.")
|
|
.context("setting service description")?;
|
|
|
|
tracing::info!(service = SERVICE_NAME, "installed Windows service");
|
|
Ok(())
|
|
}
|
|
|
|
/// Removes the service registration. Does not stop a currently-running
|
|
/// instance first — stop it via `services.msc`/`sc.exe stop` before
|
|
/// uninstalling if it's running.
|
|
pub fn uninstall() -> Result<()> {
|
|
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
|
|
.context("opening Service Control Manager")?;
|
|
let service = manager
|
|
.open_service(SERVICE_NAME, ServiceAccess::DELETE)
|
|
.context("opening service for deletion")?;
|
|
service.delete().context("deleting service")?;
|
|
|
|
tracing::info!(service = SERVICE_NAME, "removed Windows service");
|
|
Ok(())
|
|
}
|
|
|
|
define_windows_service!(ffi_service_main, service_main);
|
|
|
|
/// Blocks, handing control to the SCM dispatch loop -- this is what
|
|
/// `main()` calls for the `run-service` subcommand, which is what the SCM
|
|
/// itself launches when the service starts. Must not be called from
|
|
/// inside a tokio runtime (see the doc comment on `main()` in main.rs).
|
|
pub fn run_as_service() -> Result<()> {
|
|
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
|
.context("starting Windows service dispatcher")
|
|
}
|
|
|
|
fn service_main(_arguments: Vec<OsString>) {
|
|
if let Err(e) = run_service() {
|
|
// Nowhere better to put this yet -- see the module-level caveat
|
|
// about tracing having no attached console under the SCM.
|
|
tracing::error!(error = ?e, "windows service run failed");
|
|
}
|
|
}
|
|
|
|
fn run_service() -> Result<()> {
|
|
let (shutdown_tx, shutdown_rx) = std::sync::mpsc::channel::<()>();
|
|
|
|
let event_handler = move |control_event| -> ServiceControlHandlerResult {
|
|
match control_event {
|
|
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
|
ServiceControl::Stop => {
|
|
let _ = shutdown_tx.send(());
|
|
ServiceControlHandlerResult::NoError
|
|
}
|
|
_ => ServiceControlHandlerResult::NotImplemented,
|
|
}
|
|
};
|
|
|
|
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)
|
|
.context("registering service control handler")?;
|
|
|
|
status_handle
|
|
.set_service_status(ServiceStatus {
|
|
service_type: SERVICE_TYPE,
|
|
current_state: ServiceState::Running,
|
|
controls_accepted: ServiceControlAccept::STOP,
|
|
exit_code: ServiceExitCode::Win32(0),
|
|
checkpoint: 0,
|
|
wait_hint: Duration::default(),
|
|
process_id: None,
|
|
})
|
|
.context("reporting Running status to the SCM")?;
|
|
|
|
// service_main is invoked by the SCM on a plain thread, not an async
|
|
// context -- build a dedicated tokio runtime here and run the actual
|
|
// agent on it, same `run_agent` entry point a normal foreground run
|
|
// uses. Block this thread until either the agent exits on its own or
|
|
// the SCM asks us to stop.
|
|
let agent_thread = std::thread::spawn(|| {
|
|
let rt = match tokio::runtime::Runtime::new() {
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
tracing::error!(error = %e, "building tokio runtime for service run");
|
|
return;
|
|
}
|
|
};
|
|
if let Err(e) = rt.block_on(crate::run_agent(None)) {
|
|
tracing::error!(error = %e, "agent exited with error while running as a service");
|
|
}
|
|
});
|
|
|
|
loop {
|
|
if shutdown_rx.recv_timeout(Duration::from_millis(500)).is_ok() {
|
|
break;
|
|
}
|
|
if agent_thread.is_finished() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
status_handle
|
|
.set_service_status(ServiceStatus {
|
|
service_type: SERVICE_TYPE,
|
|
current_state: ServiceState::Stopped,
|
|
controls_accepted: ServiceControlAccept::empty(),
|
|
exit_code: ServiceExitCode::Win32(0),
|
|
checkpoint: 0,
|
|
wait_hint: Duration::default(),
|
|
process_id: None,
|
|
})
|
|
.context("reporting Stopped status to the SCM")?;
|
|
|
|
Ok(())
|
|
}
|