4 Commits

Author SHA1 Message Date
CleverWild
64a07e0ed6 docs(service): clarify ACL setup requirements for service and interactive user access
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
ci/woodpecker/pr/useragent-analyze Pipeline failed
2026-04-03 01:54:25 +02:00
CleverWild
f245a6575d fix(service): change service start type from OnDemand to AutoStart 2026-04-03 01:49:37 +02:00
CleverWild
e3050bc5ff refactor(server): inline runtime.rs in the root module 2026-04-03 01:45:09 +02:00
CleverWild
d593eedf01 housekeeping(cli): move DEFAULT_SERVER_PORT upper to exports scope 2026-04-03 01:37:12 +02:00
6 changed files with 89 additions and 92 deletions

View File

@@ -31,6 +31,7 @@ pub struct ClientMetadata {
} }
pub static BOOTSTRAP_PATH: &str = "bootstrap_token"; pub static BOOTSTRAP_PATH: &str = "bootstrap_token";
pub const DEFAULT_SERVER_PORT: u16 = 50051;
static HOME_OVERRIDE: LazyLock<RwLock<Option<PathBuf>>> = LazyLock::new(|| RwLock::new(None)); static HOME_OVERRIDE: LazyLock<RwLock<Option<PathBuf>>> = LazyLock::new(|| RwLock::new(None));
pub fn set_home_path_override(path: Option<PathBuf>) -> Result<(), std::io::Error> { pub fn set_home_path_override(path: Option<PathBuf>) -> Result<(), std::io::Error> {

View File

@@ -1,8 +1,14 @@
use std::{net::SocketAddr, path::PathBuf}; use std::{
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
path::PathBuf,
};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:50051"; const DEFAULT_LISTEN_ADDR: SocketAddr = SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::LOCALHOST,
arbiter_proto::DEFAULT_SERVER_PORT,
));
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "arbiter-server")] #[command(name = "arbiter-server")]
@@ -25,7 +31,7 @@ pub enum Command {
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct RunArgs { pub struct RunArgs {
#[arg(long, default_value = DEFAULT_LISTEN_ADDR)] #[arg(long, default_value_t = DEFAULT_LISTEN_ADDR)]
pub listen_addr: SocketAddr, pub listen_addr: SocketAddr,
#[arg(long)] #[arg(long)]
pub data_dir: Option<PathBuf>, pub data_dir: Option<PathBuf>,
@@ -34,9 +40,7 @@ pub struct RunArgs {
impl Default for RunArgs { impl Default for RunArgs {
fn default() -> Self { fn default() -> Self {
Self { Self {
listen_addr: DEFAULT_LISTEN_ADDR listen_addr: DEFAULT_LISTEN_ADDR,
.parse()
.expect("listen address literal must be valid"),
data_dir: None, data_dir: None,
} }
} }
@@ -61,7 +65,7 @@ pub struct ServiceInstallArgs {
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct ServiceRunArgs { pub struct ServiceRunArgs {
#[arg(long, default_value = DEFAULT_LISTEN_ADDR)] #[arg(long, default_value_t = DEFAULT_LISTEN_ADDR)]
pub listen_addr: SocketAddr, pub listen_addr: SocketAddr,
#[arg(long)] #[arg(long)]
pub data_dir: Option<PathBuf>, pub data_dir: Option<PathBuf>,

View File

@@ -1,12 +1,19 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
use crate::context::ServerContext;
use std::{net::SocketAddr, path::PathBuf};
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
use miette::miette;
use tonic::transport::{Identity, ServerTlsConfig};
use tracing::info;
use crate::{actors::bootstrap::GetToken, context::ServerContext};
pub mod actors; pub mod actors;
pub mod context; pub mod context;
pub mod db; pub mod db;
pub mod evm; pub mod evm;
pub mod grpc; pub mod grpc;
pub mod runtime;
pub mod safe_cell; pub mod safe_cell;
pub mod utils; pub mod utils;
@@ -19,3 +26,64 @@ impl Server {
Self { context } Self { context }
} }
} }
#[derive(Debug, Clone)]
pub struct RunConfig {
pub addr: SocketAddr,
pub data_dir: Option<PathBuf>,
pub log_arbiter_url: bool,
}
impl RunConfig {
pub fn new(addr: SocketAddr, data_dir: Option<PathBuf>) -> Self {
Self {
addr,
data_dir,
log_arbiter_url: true,
}
}
}
pub async fn run_server_until_shutdown<F>(config: RunConfig, shutdown: F) -> miette::Result<()>
where
F: Future<Output = ()> + Send + 'static,
{
arbiter_proto::set_home_path_override(config.data_dir.clone())
.map_err(|err| miette!("failed to set home path override: {err}"))?;
let db = db::create_pool(None).await?;
info!(addr = %config.addr, "Database ready");
let context = ServerContext::new(db).await?;
info!(addr = %config.addr, "Server context ready");
if config.log_arbiter_url {
let url = ArbiterUrl {
host: config.addr.ip().to_string(),
port: config.addr.port(),
ca_cert: context.tls.ca_cert().clone().into_owned(),
bootstrap_token: context
.actors
.bootstrapper
.ask(GetToken)
.await
.map_err(|err| miette!("failed to get bootstrap token from actor: {err}"))?,
};
info!(%url, "Server URL");
}
let tls = ServerTlsConfig::new().identity(Identity::from_pem(
context.tls.cert_pem(),
context.tls.key_pem(),
));
tonic::transport::Server::builder()
.tls_config(tls)
.map_err(|err| miette!("Failed to setup TLS: {err}"))?
.add_service(ArbiterServiceServer::new(Server::new(context)))
.serve_with_shutdown(config.addr, shutdown)
.await
.map_err(|e| miette!("gRPC server error: {e}"))?;
Ok(())
}

View File

@@ -25,8 +25,8 @@ async fn main() -> miette::Result<()> {
async fn run_foreground(args: RunArgs) -> miette::Result<()> { async fn run_foreground(args: RunArgs) -> miette::Result<()> {
info!(addr = %args.listen_addr, "Starting arbiter server"); info!(addr = %args.listen_addr, "Starting arbiter server");
arbiter_server::runtime::run_server_until_shutdown( arbiter_server::run_server_until_shutdown(
arbiter_server::runtime::RunConfig::new(args.listen_addr, args.data_dir), arbiter_server::RunConfig::new(args.listen_addr, args.data_dir),
std::future::pending::<()>(), std::future::pending::<()>(),
) )
.await .await

View File

@@ -1,77 +0,0 @@
use std::{future::Future, net::SocketAddr, path::PathBuf};
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
use kameo::actor::ActorRef;
use miette::miette;
use tonic::transport::{Identity, ServerTlsConfig};
use tracing::info;
use crate::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
#[derive(Debug, Clone)]
pub struct RunConfig {
pub addr: SocketAddr,
pub data_dir: Option<PathBuf>,
pub log_arbiter_url: bool,
}
impl RunConfig {
pub fn new(addr: SocketAddr, data_dir: Option<PathBuf>) -> Self {
Self {
addr,
data_dir,
log_arbiter_url: true,
}
}
}
pub async fn run_server_until_shutdown<F>(config: RunConfig, shutdown: F) -> miette::Result<()>
where
F: Future<Output = ()> + Send + 'static,
{
arbiter_proto::set_home_path_override(config.data_dir.clone())
.map_err(|err| miette!("failed to set home path override: {err}"))?;
let db = db::create_pool(None).await?;
info!(addr = %config.addr, "Database ready");
let context = ServerContext::new(db).await?;
info!(addr = %config.addr, "Server context ready");
if config.log_arbiter_url {
let url =
build_arbiter_url(config.addr, &context.actors.bootstrapper, &context.tls).await?;
info!(%url, "Server URL");
}
let tls = ServerTlsConfig::new().identity(Identity::from_pem(
context.tls.cert_pem(),
context.tls.key_pem(),
));
tonic::transport::Server::builder()
.tls_config(tls)
.map_err(|err| miette!("Failed to setup TLS: {err}"))?
.add_service(ArbiterServiceServer::new(Server::new(context)))
.serve_with_shutdown(config.addr, shutdown)
.await
.map_err(|e| miette!("gRPC server error: {e}"))?;
Ok(())
}
async fn build_arbiter_url(
addr: SocketAddr,
bootstrapper: &ActorRef<crate::actors::bootstrap::Bootstrapper>,
tls: &crate::context::tls::TlsManager,
) -> miette::Result<ArbiterUrl> {
Ok(ArbiterUrl {
host: addr.ip().to_string(),
port: addr.port(),
ca_cert: tls.ca_cert().clone().into_owned(),
bootstrap_token: bootstrapper
.ask(GetToken)
.await
.map_err(|err| miette!("failed to get bootstrap token from actor: {err}"))?,
})
}

View File

@@ -19,7 +19,7 @@ use windows_service::{
}; };
use crate::cli::{ServiceInstallArgs, ServiceRunArgs}; use crate::cli::{ServiceInstallArgs, ServiceRunArgs};
use arbiter_server::runtime::{RunConfig, run_server_until_shutdown}; use arbiter_server::{RunConfig, run_server_until_shutdown};
const SERVICE_NAME: &str = "ArbiterServer"; const SERVICE_NAME: &str = "ArbiterServer";
const SERVICE_DISPLAY_NAME: &str = "Arbiter Server"; const SERVICE_DISPLAY_NAME: &str = "Arbiter Server";
@@ -58,7 +58,7 @@ pub fn install_service(args: ServiceInstallArgs) -> miette::Result<()> {
name: OsString::from(SERVICE_NAME), name: OsString::from(SERVICE_NAME),
display_name: OsString::from(SERVICE_DISPLAY_NAME), display_name: OsString::from(SERVICE_DISPLAY_NAME),
service_type: ServiceType::OWN_PROCESS, service_type: ServiceType::OWN_PROCESS,
start_type: ServiceStartType::OnDemand, start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal, error_control: ServiceErrorControl::Normal,
executable_path: executable, executable_path: executable,
launch_arguments, launch_arguments,
@@ -203,8 +203,9 @@ fn ensure_admin_rights() -> miette::Result<()> {
} }
fn ensure_token_acl_contract(data_dir: &Path) -> miette::Result<()> { fn ensure_token_acl_contract(data_dir: &Path) -> miette::Result<()> {
// IMPORTANT: This ACL setup is intentionally explicit and should not be simplified away, // IMPORTANT: Keep this ACL setup explicit.
// because service-account and interactive-user access requirements are different in production. // The service account needs write access, while the interactive user only needs read access
// to the bootstrap token and service data directory.
let target = data_dir.as_os_str(); let target = data_dir.as_os_str();
let status = Command::new("icacls") let status = Command::new("icacls")