use std::env;
use std::error::Error;
use std::fs;
use std::net::SocketAddr;
use std::sync::Arc;
use rpc_runtime_server::RpcServerSecurityConfig;
use tripley_native_core::{
ConfiguredNativePolicy, KioskRuntimeAuthority, KioskRuntimeAuthorityOptions,
NativePolicyConfig, NativeRpcServerOptions, NativeServiceSet,
build_native_rpc_server_with_options,
};
#[cfg(feature = "transport-websocket")]
mod resilient_websocket_listener;
#[cfg(not(any(feature = "transport-websocket", feature = "transport-tcp")))]
compile_error!("tripley-native-hostd requires at least one transport feature");
const DEFAULT_ADDR: &str = "127.0.0.1:0";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TransportKind {
WebSocket,
Tcp,
}
#[derive(Default)]
struct RawOptions {
transport: Option<String>,
addr: Option<String>,
services: Option<String>,
policy_file: Option<String>,
policy_json: Option<String>,
dev_permissive: Option<bool>,
auth_token: Option<String>,
remote_allowed: Option<bool>,
max_message_size: Option<usize>,
xfs_dev_mock: Option<bool>,
xfs_dll_directory: Option<String>,
xfs_control_simulator_ws_url: Option<String>,
xfs_initial_generation: Option<u64>,
xfs_command_leases: Option<String>,
xfs_protection_file: Option<String>,
kiosk_runtime_leases: Option<String>,
terminal_id: Option<String>,
kiosk_runtime_journal_file: Option<String>,
kiosk_runtime_ttl_ms: Option<u64>,
kiosk_runtime_reconnect_grace_ms: Option<u64>,
kiosk_runtime_preferred_owner_wait_ms: Option<u64>,
}
struct HostOptions {
transport: TransportKind,
addr: SocketAddr,
services: NativeServiceSet,
xfs: bool,
xfs_control: bool,
xfs_dev_mock: bool,
xfs_dll_directory: Option<String>,
xfs_control_simulator_ws_url: Option<String>,
xfs_initial_generation: u64,
xfs_command_lease_mode: String,
#[cfg(feature = "service-xfs")]
xfs_protection: Option<tripley_native_xfs::XfsProtectionConfig>,
policy: NativePolicyConfig,
auth_token: Option<String>,
remote_allowed: bool,
max_message_size: usize,
kiosk_runtime: Option<KioskRuntimeAuthority>,
}
#[tokio::main]
async fn main() {
if let Err(error) = run().await {
eprintln!("tripley-native-hostd: {error}");
std::process::exit(1);
}
}
async fn run() -> Result<(), Box<dyn Error>> {
let options = parse_options()?;
let mut security = if options.remote_allowed {
RpcServerSecurityConfig::default().remote_allowed()
} else {
RpcServerSecurityConfig::default().local_only()
};
if let Some(token) = options.auth_token {
security = security.with_token(token);
}
let mut providers: Vec<Arc<dyn tripley_native_core::NativeRpcProvider>> = Vec::new();
if options.xfs {
#[cfg(feature = "service-xfs")]
{
providers.push(Arc::new(tripley_native_xfs::XfsProvider::try_new(
tripley_native_xfs::XfsProviderOptions {
dll_directory: options.xfs_dll_directory.clone(),
dev_mock: options.xfs_dev_mock,
initial_generation: options.xfs_initial_generation,
command_lease_mode: options.xfs_command_lease_mode.parse()?,
protection: options.xfs_protection.clone(),
},
)?));
}
#[cfg(not(feature = "service-xfs"))]
{
return Err("service `xfs` was not compiled into this binary".into());
}
}
if options.xfs_control {
#[cfg(feature = "service-xfs-control")]
{
providers.push(Arc::new(
tripley_native_xfs_control::XfsControlProvider::new(
tripley_native_xfs_control::XfsControlProviderOptions {
simulator_ws_url: options
.xfs_control_simulator_ws_url
.clone()
.unwrap_or_else(|| {
tripley_native_xfs_control::DEFAULT_SIMULATOR_WS_URL.to_string()
}),
..tripley_native_xfs_control::XfsControlProviderOptions::default()
},
),
));
}
#[cfg(not(feature = "service-xfs-control"))]
{
return Err("service `xfs-control` was not compiled into this binary".into());
}
}
let server = build_native_rpc_server_with_options(NativeRpcServerOptions {
policy: Arc::new(ConfiguredNativePolicy::new(options.policy)),
services: options.services,
security,
providers,
kiosk_runtime: options.kiosk_runtime.clone(),
});
match options.transport {
TransportKind::WebSocket => {
#[cfg(feature = "transport-websocket")]
{
use resilient_websocket_listener::ResilientWebSocketListener;
use rpc_runtime_transport_websocket::WebSocketConfig;
let listener = ResilientWebSocketListener::bind(
options.addr,
WebSocketConfig {
max_message_size: options.max_message_size,
},
)
.await?;
let addr = listener.local_addr()?;
print_started(
"websocket",
addr,
options.services,
options.xfs,
options.xfs_control,
options.kiosk_runtime.is_some(),
);
server.serve_listener(listener).await?;
}
#[cfg(not(feature = "transport-websocket"))]
{
return Err("websocket transport was not compiled into this binary".into());
}
}
TransportKind::Tcp => {
#[cfg(feature = "transport-tcp")]
{
use rpc_runtime_transport_ipc::{FrameConfig, IpcEndpoint, IpcListener};
let listener = IpcListener::bind(
IpcEndpoint::tcp(options.addr),
FrameConfig {
max_frame_size: options.max_message_size,
},
)
.await?;
let addr = listener
.local_addr()
.ok_or("tcp listener did not expose a local address")?;
print_started(
"tcp",
addr,
options.services,
options.xfs,
options.xfs_control,
options.kiosk_runtime.is_some(),
);
server.serve_listener(listener).await?;
}
#[cfg(not(feature = "transport-tcp"))]
{
return Err("tcp transport was not compiled into this binary".into());
}
}
}
Ok(())
}
fn parse_options() -> Result<HostOptions, Box<dyn Error>> {
let raw = merge_options(env_options(), cli_options()?);
let transport = parse_transport(raw.transport.as_deref().unwrap_or("websocket"))?;
let addr = raw.addr.as_deref().unwrap_or(DEFAULT_ADDR).parse()?;
let (services, xfs, xfs_control) = parse_services(raw.services.as_deref().unwrap_or("all"))?;
let dev_permissive = raw.dev_permissive.unwrap_or(false);
let remote_allowed = raw.remote_allowed.unwrap_or(false);
let max_message_size = raw.max_message_size.unwrap_or(16 * 1024 * 1024);
let xfs_dev_mock = raw.xfs_dev_mock.unwrap_or(false);
let xfs_command_lease_mode =
parse_xfs_command_lease_mode(raw.xfs_command_leases.as_deref().unwrap_or("optional"))?;
#[cfg(feature = "service-xfs")]
let xfs_protection = raw
.xfs_protection_file
.as_ref()
.map(|path| {
let json = fs::read_to_string(path)
.map_err(|error| format!("read XFS protection file `{path}`: {error}"))?;
tripley_native_xfs::XfsProtectionConfig::from_json(&json)
})
.transpose()?;
let kiosk_runtime_mode = raw.kiosk_runtime_leases.as_deref().unwrap_or("disabled");
if !matches!(kiosk_runtime_mode, "disabled" | "required") {
return Err("kiosk runtime lease mode must be disabled or required".into());
}
let kiosk_runtime = if kiosk_runtime_mode == "required" {
let terminal_id = raw
.terminal_id
.as_deref()
.ok_or("--terminal-id is required when kiosk runtime leases are required")?;
let journal = raw
.kiosk_runtime_journal_file
.as_deref()
.ok_or("--kiosk-runtime-journal-file is required when kiosk runtime leases are required")?;
let mut runtime_options = KioskRuntimeAuthorityOptions::new(terminal_id, journal);
runtime_options.lease_ttl_ms = raw.kiosk_runtime_ttl_ms.unwrap_or(10_000);
runtime_options.reconnect_grace_ms =
raw.kiosk_runtime_reconnect_grace_ms.unwrap_or(5_000);
runtime_options.preferred_owner_wait_ms =
raw.kiosk_runtime_preferred_owner_wait_ms.unwrap_or(10_000);
Some(KioskRuntimeAuthority::open(runtime_options)?)
} else {
None
};
if raw.policy_file.is_some() && raw.policy_json.is_some() {
return Err("only one of --policy-file or --policy-json can be provided".into());
}
if dev_permissive && (raw.policy_file.is_some() || raw.policy_json.is_some()) {
return Err("--dev-permissive cannot be combined with policy JSON".into());
}
let policy = if dev_permissive {
NativePolicyConfig::dev_permissive()
} else if let Some(path) = raw.policy_file {
NativePolicyConfig::from_json(&fs::read_to_string(path)?)?
} else if let Some(json) = raw.policy_json {
NativePolicyConfig::from_json(&json)?
} else {
NativePolicyConfig::default()
};
Ok(HostOptions {
transport,
addr,
services,
xfs,
xfs_control,
xfs_dev_mock,
xfs_dll_directory: raw.xfs_dll_directory,
xfs_control_simulator_ws_url: raw.xfs_control_simulator_ws_url,
xfs_initial_generation: raw.xfs_initial_generation.unwrap_or(0),
xfs_command_lease_mode,
#[cfg(feature = "service-xfs")]
xfs_protection,
policy,
auth_token: raw.auth_token,
remote_allowed,
max_message_size,
kiosk_runtime,
})
}
fn env_options() -> RawOptions {
RawOptions {
transport: env::var("TRIPLEY_NATIVE_HOSTD_TRANSPORT").ok(),
addr: env::var("TRIPLEY_NATIVE_HOSTD_ADDR").ok(),
services: env::var("TRIPLEY_NATIVE_HOSTD_SERVICES").ok(),
policy_file: env::var("TRIPLEY_NATIVE_HOSTD_POLICY_FILE").ok(),
policy_json: env::var("TRIPLEY_NATIVE_HOSTD_POLICY_JSON").ok(),
dev_permissive: env_bool("TRIPLEY_NATIVE_HOSTD_DEV_PERMISSIVE"),
auth_token: env::var("TRIPLEY_NATIVE_HOSTD_AUTH_TOKEN").ok(),
remote_allowed: env_bool("TRIPLEY_NATIVE_HOSTD_REMOTE_ALLOWED"),
max_message_size: env_usize("TRIPLEY_NATIVE_HOSTD_MAX_MESSAGE_SIZE"),
xfs_dev_mock: env_bool("TRIPLEY_NATIVE_HOSTD_XFS_DEV_MOCK"),
xfs_dll_directory: env::var("TRIPLEY_NATIVE_HOSTD_XFS_DLL_DIRECTORY").ok(),
xfs_control_simulator_ws_url: env::var("TRIPLEY_NATIVE_HOSTD_XFS_CONTROL_SIMULATOR_WS_URL")
.ok(),
xfs_initial_generation: env_u64("TRIPLEY_NATIVE_HOSTD_XFS_INITIAL_GENERATION"),
xfs_command_leases: env::var("TRIPLEY_NATIVE_HOSTD_XFS_COMMAND_LEASES").ok(),
xfs_protection_file: env::var("TRIPLEY_NATIVE_HOSTD_XFS_PROTECTION_FILE").ok(),
kiosk_runtime_leases: env::var("TRIPLEY_NATIVE_HOSTD_KIOSK_RUNTIME_LEASES").ok(),
terminal_id: env::var("TRIPLEY_NATIVE_HOSTD_TERMINAL_ID").ok(),
kiosk_runtime_journal_file: env::var("TRIPLEY_NATIVE_HOSTD_KIOSK_RUNTIME_JOURNAL_FILE").ok(),
kiosk_runtime_ttl_ms: env_u64("TRIPLEY_NATIVE_HOSTD_KIOSK_RUNTIME_TTL_MS"),
kiosk_runtime_reconnect_grace_ms: env_u64("TRIPLEY_NATIVE_HOSTD_KIOSK_RUNTIME_RECONNECT_GRACE_MS"),
kiosk_runtime_preferred_owner_wait_ms: env_u64("TRIPLEY_NATIVE_HOSTD_KIOSK_RUNTIME_PREFERRED_OWNER_WAIT_MS"),
}
}
fn cli_options() -> Result<RawOptions, Box<dyn Error>> {
let mut raw = RawOptions::default();
let args = env::args().skip(1).collect::<Vec<_>>();
let mut index = 0;
while index < args.len() {
let arg = &args[index];
if arg == "--help" || arg == "-h" {
print_help();
std::process::exit(0);
}
let (key, inline_value) = arg
.split_once('=')
.map(|(key, value)| (key, Some(value.to_string())))
.unwrap_or((arg.as_str(), None));
match key {
"--transport" => raw.transport = Some(value(&args, &mut index, inline_value)?),
"--addr" => raw.addr = Some(value(&args, &mut index, inline_value)?),
"--services" => raw.services = Some(value(&args, &mut index, inline_value)?),
"--policy-file" => raw.policy_file = Some(value(&args, &mut index, inline_value)?),
"--policy-json" => raw.policy_json = Some(value(&args, &mut index, inline_value)?),
"--auth-token" => raw.auth_token = Some(value(&args, &mut index, inline_value)?),
"--max-message-size" => {
raw.max_message_size = Some(value(&args, &mut index, inline_value)?.parse()?)
}
"--xfs-dll-directory" => {
raw.xfs_dll_directory = Some(value(&args, &mut index, inline_value)?)
}
"--xfs-control-simulator-ws-url" => {
raw.xfs_control_simulator_ws_url = Some(value(&args, &mut index, inline_value)?)
}
"--xfs-command-leases" => {
raw.xfs_command_leases = Some(value(&args, &mut index, inline_value)?)
}
"--xfs-protection-file" => {
raw.xfs_protection_file = Some(value(&args, &mut index, inline_value)?)
}
"--kiosk-runtime-leases" => {
raw.kiosk_runtime_leases = Some(value(&args, &mut index, inline_value)?)
}
"--terminal-id" => raw.terminal_id = Some(value(&args, &mut index, inline_value)?),
"--kiosk-runtime-journal-file" => {
raw.kiosk_runtime_journal_file = Some(value(&args, &mut index, inline_value)?)
}
"--kiosk-runtime-ttl-ms" => {
raw.kiosk_runtime_ttl_ms = Some(value(&args, &mut index, inline_value)?.parse()?)
}
"--kiosk-runtime-reconnect-grace-ms" => {
raw.kiosk_runtime_reconnect_grace_ms = Some(value(&args, &mut index, inline_value)?.parse()?)
}
"--kiosk-runtime-preferred-owner-wait-ms" => {
raw.kiosk_runtime_preferred_owner_wait_ms = Some(value(&args, &mut index, inline_value)?.parse()?)
}
"--dev-permissive" => raw.dev_permissive = Some(true),
"--remote-allowed" => raw.remote_allowed = Some(true),
"--xfs-dev-mock" => raw.xfs_dev_mock = Some(true),
other => return Err(format!("unknown argument `{other}`").into()),
}
index += 1;
}
Ok(raw)
}
fn merge_options(mut env: RawOptions, cli: RawOptions) -> RawOptions {
env.transport = cli.transport.or(env.transport);
env.addr = cli.addr.or(env.addr);
env.services = cli.services.or(env.services);
env.policy_file = cli.policy_file.or(env.policy_file);
env.policy_json = cli.policy_json.or(env.policy_json);
env.dev_permissive = cli.dev_permissive.or(env.dev_permissive);
env.auth_token = cli.auth_token.or(env.auth_token);
env.remote_allowed = cli.remote_allowed.or(env.remote_allowed);
env.max_message_size = cli.max_message_size.or(env.max_message_size);
env.xfs_dev_mock = cli.xfs_dev_mock.or(env.xfs_dev_mock);
env.xfs_dll_directory = cli.xfs_dll_directory.or(env.xfs_dll_directory);
env.xfs_control_simulator_ws_url = cli
.xfs_control_simulator_ws_url
.or(env.xfs_control_simulator_ws_url);
env.xfs_initial_generation = cli.xfs_initial_generation.or(env.xfs_initial_generation);
env.xfs_command_leases = cli.xfs_command_leases.or(env.xfs_command_leases);
env.xfs_protection_file = cli.xfs_protection_file.or(env.xfs_protection_file);
env.kiosk_runtime_leases = cli.kiosk_runtime_leases.or(env.kiosk_runtime_leases);
env.terminal_id = cli.terminal_id.or(env.terminal_id);
env.kiosk_runtime_journal_file = cli.kiosk_runtime_journal_file.or(env.kiosk_runtime_journal_file);
env.kiosk_runtime_ttl_ms = cli.kiosk_runtime_ttl_ms.or(env.kiosk_runtime_ttl_ms);
env.kiosk_runtime_reconnect_grace_ms = cli.kiosk_runtime_reconnect_grace_ms.or(env.kiosk_runtime_reconnect_grace_ms);
env.kiosk_runtime_preferred_owner_wait_ms = cli.kiosk_runtime_preferred_owner_wait_ms.or(env.kiosk_runtime_preferred_owner_wait_ms);
env
}
fn parse_transport(value: &str) -> Result<TransportKind, Box<dyn Error>> {
match value {
"websocket" | "ws" => Ok(TransportKind::WebSocket),
"tcp" => Ok(TransportKind::Tcp),
_ => Err(format!("transport must be one of: websocket, tcp; got `{value}`").into()),
}
}
fn parse_xfs_command_lease_mode(value: &str) -> Result<String, Box<dyn Error>> {
match value {
"disabled" | "optional" | "required" => Ok(value.to_string()),
_ => Err(format!(
"XFS command lease mode must be disabled, optional, or required; got `{value}`"
)
.into()),
}
}
fn parse_services(value: &str) -> Result<(NativeServiceSet, bool, bool), Box<dyn Error>> {
if value == "all" {
return Ok((
compiled_services(),
cfg!(feature = "service-xfs"),
cfg!(feature = "service-xfs-control"),
));
}
let mut services = NativeServiceSet::runtime_only();
let mut xfs = false;
let mut xfs_control = false;
for item in value
.split(',')
.map(str::trim)
.filter(|item| !item.is_empty())
{
match item {
"runtime" => {}
"fs" => set_service(item, &mut services.fs)?,
"archive" => set_service(item, &mut services.archive)?,
"tcp" => set_service(item, &mut services.tcp)?,
"websocket" | "ws" => set_service("websocket", &mut services.websocket)?,
"sqlite" => set_service(item, &mut services.sqlite)?,
"system" => set_service(item, &mut services.system)?,
"xfs" => {
if !cfg!(feature = "service-xfs") {
return Err("service `xfs` was not compiled into this binary".into());
}
xfs = true;
}
"xfs-control" | "xfs_control" => {
if !cfg!(feature = "service-xfs-control") {
return Err("service `xfs-control` was not compiled into this binary".into());
}
xfs_control = true;
}
_ => return Err(format!("unknown service `{item}`").into()),
}
}
ensure_compiled(services)?;
Ok((services, xfs, xfs_control))
}
fn compiled_services() -> NativeServiceSet {
NativeServiceSet {
fs: cfg!(feature = "service-fs"),
archive: cfg!(feature = "service-archive"),
tcp: cfg!(feature = "service-tcp"),
websocket: cfg!(feature = "service-websocket"),
sqlite: cfg!(feature = "service-sqlite"),
system: cfg!(feature = "service-system"),
}
}
fn ensure_compiled(services: NativeServiceSet) -> Result<(), Box<dyn Error>> {
let compiled = compiled_services();
if services.fs && !compiled.fs {
return Err("service `fs` was not compiled into this binary".into());
}
if services.archive && !compiled.archive {
return Err("service `archive` was not compiled into this binary".into());
}
if services.tcp && !compiled.tcp {
return Err("service `tcp` was not compiled into this binary".into());
}
if services.websocket && !compiled.websocket {
return Err("service `websocket` was not compiled into this binary".into());
}
if services.sqlite && !compiled.sqlite {
return Err("service `sqlite` was not compiled into this binary".into());
}
if services.system && !compiled.system {
return Err("service `system` was not compiled into this binary".into());
}
Ok(())
}
fn set_service(name: &str, field: &mut bool) -> Result<(), Box<dyn Error>> {
*field = true;
ensure_compiled(match name {
"fs" => NativeServiceSet {
fs: true,
..NativeServiceSet::runtime_only()
},
"archive" => NativeServiceSet {
archive: true,
..NativeServiceSet::runtime_only()
},
"tcp" => NativeServiceSet {
tcp: true,
..NativeServiceSet::runtime_only()
},
"websocket" => NativeServiceSet {
websocket: true,
..NativeServiceSet::runtime_only()
},
"sqlite" => NativeServiceSet {
sqlite: true,
..NativeServiceSet::runtime_only()
},
"system" => NativeServiceSet {
system: true,
..NativeServiceSet::runtime_only()
},
_ => NativeServiceSet::runtime_only(),
})
}
fn value(
args: &[String],
index: &mut usize,
inline_value: Option<String>,
) -> Result<String, Box<dyn Error>> {
if let Some(value) = inline_value {
return Ok(value);
}
*index += 1;
args.get(*index)
.cloned()
.ok_or_else(|| "missing argument value".into())
}
fn env_bool(name: &str) -> Option<bool> {
env::var(name).ok().and_then(|value| match value.as_str() {
"1" | "true" | "TRUE" | "yes" | "on" => Some(true),
"0" | "false" | "FALSE" | "no" | "off" => Some(false),
_ => None,
})
}
fn env_usize(name: &str) -> Option<usize> {
env::var(name).ok().and_then(|value| value.parse().ok())
}
fn env_u64(name: &str) -> Option<u64> {
env::var(name).ok().and_then(|value| value.parse().ok())
}
fn print_started(
transport: &str,
addr: SocketAddr,
services: NativeServiceSet,
xfs: bool,
xfs_control: bool,
kiosk_runtime: bool,
) {
let mut capabilities = services.capabilities();
if xfs {
capabilities.extend([
"xfs",
"xfs.manager",
"xfs.idc",
"xfs.pin",
"xfs.bcr",
"xfs.cdm",
"xfs.cim",
"xfs.ptr",
"xfs.siu",
"xfs.ttu",
"xfs.vdm",
"xfs.command-leases",
]);
}
if xfs_control {
capabilities.extend([
"xfs.control",
"xfs.control.runtime",
"xfs.control.idc",
"xfs.control.pin",
"xfs.control.bcr",
"xfs.control.cdm",
"xfs.control.cim",
"xfs.control.ptr",
"xfs.control.siu",
"xfs.control.ttu",
]);
}
if kiosk_runtime {
capabilities.push("runtime.customer-lease");
}
println!(
"{{\"event\":\"started\",\"transport\":\"{}\",\"addr\":\"{}\",\"services\":[{}]}}",
transport,
addr,
capabilities
.into_iter()
.map(|value| format!("\"{value}\""))
.collect::<Vec<_>>()
.join(",")
);
}
fn print_help() {
println!(
"Usage: tripley-native-hostd [--transport websocket|tcp] [--addr host:port] [--services all|runtime,fs,archive,tcp,websocket,sqlite,system,xfs,xfs-control] [--policy-file path|--policy-json json] [--dev-permissive] [--auth-token token] [--remote-allowed] [--max-message-size bytes] [--xfs-dll-directory path] [--xfs-dev-mock] [--xfs-command-leases disabled|optional|required] [--xfs-protection-file path] [--xfs-control-simulator-ws-url ws://host:port] [--kiosk-runtime-leases disabled|required] [--terminal-id id] [--kiosk-runtime-journal-file path] [--kiosk-runtime-ttl-ms 10000] [--kiosk-runtime-reconnect-grace-ms 5000] [--kiosk-runtime-preferred-owner-wait-ms 10000]"
);
}