From 3cc13ccf1edbe260eb4cbb6278c4633365ed9db4 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 16 Apr 2026 12:59:31 +0800 Subject: [PATCH] feat(ops): add operation-system app skeleton --- Cargo.lock | 1 + crates/app_operation_system/Cargo.toml | 3 + crates/app_operation_system/src/app.rs | 69 +++++++++++++++++++ crates/app_operation_system/src/lib.rs | 5 ++ crates/app_operation_system/src/main.rs | 7 +- crates/app_operation_system/src/router.rs | 20 ++++++ .../tests/router_smoke.rs | 23 +++++++ crates/app_operation_system/web/index.html | 14 ++++ 8 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 crates/app_operation_system/src/app.rs create mode 100644 crates/app_operation_system/src/lib.rs create mode 100644 crates/app_operation_system/src/router.rs create mode 100644 crates/app_operation_system/tests/router_smoke.rs create mode 100644 crates/app_operation_system/web/index.html diff --git a/Cargo.lock b/Cargo.lock index 0461b45..427a67e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,7 @@ dependencies = [ "dotenv", "plc_platform_core", "tokio", + "tower", "tower-http", "tracing", ] diff --git a/crates/app_operation_system/Cargo.toml b/crates/app_operation_system/Cargo.toml index 85d661f..d7ec056 100644 --- a/crates/app_operation_system/Cargo.toml +++ b/crates/app_operation_system/Cargo.toml @@ -11,6 +11,9 @@ tower-http = { version = "0.6", features = ["cors", "fs"] } tracing = "0.1" dotenv = "0.15" +[dev-dependencies] +tower = { version = "0.5", features = ["util"] } + [[bin]] name = "app_operation_system" path = "src/main.rs" diff --git a/crates/app_operation_system/src/app.rs b/crates/app_operation_system/src/app.rs new file mode 100644 index 0000000..ce1efc1 --- /dev/null +++ b/crates/app_operation_system/src/app.rs @@ -0,0 +1,69 @@ +use crate::router::build_router; + +#[derive(Clone, Debug)] +pub struct AppConfig { + pub server_host: String, + pub server_port: u16, +} + +impl AppConfig { + pub fn from_env() -> Self { + Self { + server_host: std::env::var("OPS_SERVER_HOST") + .unwrap_or_else(|_| "127.0.0.1".to_string()), + server_port: std::env::var("OPS_SERVER_PORT") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(3100), + } + } +} + +#[derive(Clone, Debug)] +pub struct AppState { + pub app_name: &'static str, + pub config: AppConfig, +} + +pub async fn run() { + dotenv::dotenv().ok(); + plc_platform_core::util::log::init_logger(); + let _platform = plc_platform_core::bootstrap::bootstrap_platform(); + let _single_instance = + match plc_platform_core::util::single_instance::try_acquire("PLCControl.OperationSystem") { + Ok(guard) => guard, + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { + tracing::warn!("Another operation-system instance is already running"); + return; + } + Err(err) => { + tracing::error!("Failed to initialize single instance guard: {}", err); + return; + } + }; + + let state = AppState { + app_name: "operation-system", + config: AppConfig::from_env(), + }; + let app = build_router(state.clone()); + let addr = format!("{}:{}", state.config.server_host, state.config.server_port); + tracing::info!("Starting operation-system server at http://{}", addr); + let listener = tokio::net::TcpListener::bind(&addr) + .await + .expect("operation-system listener should bind"); + + axum::serve(listener, app) + .await + .expect("operation-system server should run"); +} + +pub fn test_state() -> AppState { + AppState { + app_name: "operation-system", + config: AppConfig { + server_host: "127.0.0.1".to_string(), + server_port: 0, + }, + } +} diff --git a/crates/app_operation_system/src/lib.rs b/crates/app_operation_system/src/lib.rs new file mode 100644 index 0000000..de705cd --- /dev/null +++ b/crates/app_operation_system/src/lib.rs @@ -0,0 +1,5 @@ +pub mod app; +pub mod router; + +pub use app::{run, test_state, AppState}; +pub use router::build_router; diff --git a/crates/app_operation_system/src/main.rs b/crates/app_operation_system/src/main.rs index e71fdf5..679c59b 100644 --- a/crates/app_operation_system/src/main.rs +++ b/crates/app_operation_system/src/main.rs @@ -1 +1,6 @@ -fn main() {} \ No newline at end of file +#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] + +#[tokio::main] +async fn main() { + app_operation_system::run().await; +} diff --git a/crates/app_operation_system/src/router.rs b/crates/app_operation_system/src/router.rs new file mode 100644 index 0000000..9b63112 --- /dev/null +++ b/crates/app_operation_system/src/router.rs @@ -0,0 +1,20 @@ +use axum::{extract::State, routing::get, Router}; +use tower_http::services::ServeDir; + +use crate::app::AppState; + +const WEB_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/web"); + +pub fn build_router(state: AppState) -> Router { + Router::new() + .route("/api/health", get(health_check)) + .nest_service( + "/ui", + ServeDir::new(WEB_ROOT).append_index_html_on_directories(true), + ) + .with_state(state) +} + +async fn health_check(State(state): State) -> String { + format!("{}:ok", state.app_name) +} diff --git a/crates/app_operation_system/tests/router_smoke.rs b/crates/app_operation_system/tests/router_smoke.rs new file mode 100644 index 0000000..d621193 --- /dev/null +++ b/crates/app_operation_system/tests/router_smoke.rs @@ -0,0 +1,23 @@ +use axum::{ + body::Body, + http::{Method, Request, StatusCode}, +}; +use tower::ServiceExt; + +#[tokio::test] +async fn operation_system_router_exposes_health_endpoint() { + let app = app_operation_system::build_router(app_operation_system::app::test_state()); + + let response = app + .oneshot( + Request::builder() + .method(Method::GET) + .uri("/api/health") + .body(Body::empty()) + .expect("request should build"), + ) + .await + .expect("router should answer request"); + + assert_eq!(response.status(), StatusCode::OK); +} diff --git a/crates/app_operation_system/web/index.html b/crates/app_operation_system/web/index.html new file mode 100644 index 0000000..1471a65 --- /dev/null +++ b/crates/app_operation_system/web/index.html @@ -0,0 +1,14 @@ + + + + + + PLC Control Operation System + + +
+

Operation System

+

This web root is a placeholder for the operation-system app skeleton.

+
+ +