mirror of
https://github.com/orbien-org/orbien.git
synced 2026-09-22 00:01:31 +00:00
refactor: 拆分客户端lib库用于桌面程序和CLI、修复客户端停止时未正确关闭控制连接问题、删除Tauri桌面模块
This commit is contained in:
@@ -10,15 +10,6 @@
|
||||
/server/assets/
|
||||
.orbien.run_id
|
||||
|
||||
/desktop/src-tauri/binaries/orbien-*
|
||||
!/desktop/src-tauri/binaries/README.md
|
||||
!/desktop/src-tauri/binaries/.gitkeep
|
||||
/desktop/src-tauri/gen/schemas
|
||||
/desktop/src/assets/*.icns
|
||||
/desktop/src/assets/app-icon.png
|
||||
/desktop/src/assets/logo_origin.png
|
||||
/desktop/pnpm-lock.yaml
|
||||
|
||||
node_modules/
|
||||
dist-ssr/
|
||||
.vite/
|
||||
|
||||
Generated
+5183
-24
File diff suppressed because it is too large
Load Diff
+3
-4
@@ -4,9 +4,7 @@ members = [
|
||||
"core",
|
||||
"client",
|
||||
"server",
|
||||
]
|
||||
exclude = [
|
||||
"desktop/src-tauri",
|
||||
"desktop",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -29,8 +27,9 @@ opt-level = "z"
|
||||
|
||||
[workspace.dependencies]
|
||||
orbien-core = { path = "core" }
|
||||
orbien-client = { path = "client" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["io", "compat"] }
|
||||
tokio-util = { version = "0.7", features = ["io", "compat", "rt"] }
|
||||
futures = "0.3"
|
||||
yamux = "0.13"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
+6
-1
@@ -3,7 +3,11 @@ name = "orbien-client"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "orbien client — TCP proxy over TCP/QUIC"
|
||||
description = "Orbien client library and CLI"
|
||||
|
||||
[lib]
|
||||
name = "orbien_client"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "orbien"
|
||||
@@ -12,6 +16,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
orbien-core = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
+102
-21
@@ -5,23 +5,26 @@ use anyhow::{anyhow, Result};
|
||||
use orbien_core::auth;
|
||||
use orbien_core::config::ClientConfig;
|
||||
use orbien_core::msg::{self, Login, Message, NewProxy, NewWorkConn, Ping};
|
||||
use orbien_core::transport::DynStream;
|
||||
use orbien_core::VERSION;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::{AsyncWriteExt, ReadHalf, WriteHalf};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::interval;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SessionEnd {
|
||||
Disconnected { run_id: String },
|
||||
Kicked { run_id: String, reason: String },
|
||||
}
|
||||
use orbien_core::transport::DynStream;
|
||||
use orbien_core::VERSION;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::{ReadHalf, WriteHalf};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::interval;
|
||||
|
||||
type CtrlRead = ReadHalf<DynStream>;
|
||||
type CtrlWrite = WriteHalf<DynStream>;
|
||||
type OnProxyRemote = Arc<dyn Fn(String, String) + Send + Sync>;
|
||||
|
||||
pub struct Control {
|
||||
cfg: ClientConfig,
|
||||
@@ -30,6 +33,9 @@ pub struct Control {
|
||||
writer: Mutex<CtrlWrite>,
|
||||
proxies: ProxyManager,
|
||||
connector: Arc<dyn Connector>,
|
||||
cancel: CancellationToken,
|
||||
work_tasks: Mutex<JoinSet<()>>,
|
||||
on_proxy_remote: OnProxyRemote,
|
||||
}
|
||||
|
||||
impl Control {
|
||||
@@ -37,7 +43,11 @@ impl Control {
|
||||
cfg: &ClientConfig,
|
||||
previous_run_id: String,
|
||||
config_path: &Path,
|
||||
parent_cancel: CancellationToken,
|
||||
on_connected: impl FnOnce(),
|
||||
on_proxy_remote: OnProxyRemote,
|
||||
) -> Result<SessionEnd> {
|
||||
let session_cancel = parent_cancel.child_token();
|
||||
let connector = build_connector(cfg).await?;
|
||||
let mut stream = connector.open().await?;
|
||||
tracing::info!(
|
||||
@@ -96,16 +106,28 @@ impl Control {
|
||||
writer: Mutex::new(writer),
|
||||
proxies: ProxyManager::from_config(cfg)?,
|
||||
connector,
|
||||
cancel: session_cancel.clone(),
|
||||
work_tasks: Mutex::new(JoinSet::new()),
|
||||
on_proxy_remote,
|
||||
});
|
||||
|
||||
ctl.register_all_proxies().await?;
|
||||
on_connected();
|
||||
|
||||
let hb = Arc::clone(&ctl);
|
||||
let heartbeat = tokio::spawn(async move { hb.heartbeat_loop().await });
|
||||
let hb_cancel = session_cancel.clone();
|
||||
let heartbeat = tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = hb_cancel.cancelled() => {}
|
||||
_ = hb.heartbeat_loop() => {}
|
||||
}
|
||||
});
|
||||
|
||||
let result = ctl.reader_loop().await;
|
||||
let result = ctl.clone().reader_loop().await;
|
||||
ctl.shutdown().await;
|
||||
heartbeat.abort();
|
||||
let _ = heartbeat.await;
|
||||
|
||||
match result {
|
||||
Ok(ReaderEnd::Kicked(reason)) => Ok(SessionEnd::Kicked {
|
||||
run_id: resp.run_id,
|
||||
@@ -118,6 +140,17 @@ impl Control {
|
||||
}
|
||||
}
|
||||
|
||||
async fn shutdown(&self) {
|
||||
self.cancel.cancel();
|
||||
{
|
||||
let mut writer = self.writer.lock().await;
|
||||
let _ = writer.shutdown().await;
|
||||
}
|
||||
let mut tasks = self.work_tasks.lock().await;
|
||||
tasks.abort_all();
|
||||
while tasks.join_next().await.is_some() {}
|
||||
}
|
||||
|
||||
async fn register_all_proxies(&self) -> Result<()> {
|
||||
for p in &self.cfg.proxies {
|
||||
let msg = match p.proxy_type.as_str() {
|
||||
@@ -212,11 +245,22 @@ impl Control {
|
||||
|
||||
async fn reader_loop(self: Arc<Self>) -> Result<ReaderEnd> {
|
||||
loop {
|
||||
let msg = {
|
||||
let mut reader = self.reader.lock().await;
|
||||
match msg::read_msg(&mut *reader).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => return Ok(ReaderEnd::Closed),
|
||||
if self.cancel.is_cancelled() {
|
||||
return Ok(ReaderEnd::Closed);
|
||||
}
|
||||
|
||||
let msg = tokio::select! {
|
||||
_ = self.cancel.cancelled() => {
|
||||
return Ok(ReaderEnd::Closed);
|
||||
}
|
||||
msg = async {
|
||||
let mut reader = self.reader.lock().await;
|
||||
msg::read_msg(&mut *reader).await
|
||||
} => {
|
||||
match msg {
|
||||
Ok(m) => m,
|
||||
Err(_) => return Ok(ReaderEnd::Closed),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,19 +271,30 @@ impl Control {
|
||||
}
|
||||
Message::ReqWorkConn(_) => {
|
||||
let ctl = Arc::clone(&self);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = ctl.handle_req_work_conn().await {
|
||||
tracing::error!(error = %e, "work tunnel failed");
|
||||
let cancel = self.cancel.clone();
|
||||
self.work_tasks.lock().await.spawn(async move {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => {}
|
||||
res = ctl.handle_req_work_conn() => {
|
||||
if let Err(e) = res {
|
||||
tracing::error!(error = %e, "work tunnel failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Message::NewProxyResp(resp) => {
|
||||
if resp.error.is_empty() {
|
||||
let remote = normalize_remote_addr(
|
||||
&self.cfg.server_addr,
|
||||
&resp.remote_addr,
|
||||
);
|
||||
tracing::info!(
|
||||
name = %resp.proxy_name,
|
||||
remote = %resp.remote_addr,
|
||||
remote = %remote,
|
||||
"proxy started"
|
||||
);
|
||||
(self.on_proxy_remote)(resp.proxy_name.clone(), remote);
|
||||
} else {
|
||||
tracing::error!(
|
||||
name = %resp.proxy_name,
|
||||
@@ -268,6 +323,9 @@ impl Control {
|
||||
let mut tick = interval(Duration::from_secs(secs as u64));
|
||||
tick.tick().await;
|
||||
loop {
|
||||
if self.cancel.is_cancelled() {
|
||||
break;
|
||||
}
|
||||
tick.tick().await;
|
||||
let timestamp = now_secs();
|
||||
let ping = Ping {
|
||||
@@ -298,9 +356,18 @@ impl Control {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let start = match msg::read_msg(&mut work).await? {
|
||||
Message::StartWorkConn(s) => s,
|
||||
other => return Err(anyhow!("expected StartWorkConn, got {}", other.type_byte())),
|
||||
let start = tokio::select! {
|
||||
_ = self.cancel.cancelled() => {
|
||||
return Ok(());
|
||||
}
|
||||
msg = msg::read_msg(&mut work) => {
|
||||
match msg? {
|
||||
Message::StartWorkConn(s) => s,
|
||||
other => {
|
||||
return Err(anyhow!("expected StartWorkConn, got {}", other.type_byte()))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !start.error.is_empty() {
|
||||
@@ -346,6 +413,20 @@ fn omit_client_mode(mode: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_remote_addr(server_addr: &str, remote_addr: &str) -> String {
|
||||
let remote = remote_addr.trim();
|
||||
if remote.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
if let Some(port) = remote.strip_prefix(':') {
|
||||
let host = server_addr.trim();
|
||||
if !host.is_empty() && !port.is_empty() {
|
||||
return format!("{host}:{port}");
|
||||
}
|
||||
}
|
||||
remote.to_string()
|
||||
}
|
||||
|
||||
fn new_proxy_base(
|
||||
name: &str,
|
||||
proxy_type: &str,
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
use crate::service::Service;
|
||||
use anyhow::{bail, Result};
|
||||
use orbien_core::config::ClientConfig;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ClientStatus {
|
||||
Stopped,
|
||||
Starting,
|
||||
Running,
|
||||
Reconnecting,
|
||||
Stopping,
|
||||
}
|
||||
|
||||
impl ClientStatus {
|
||||
pub fn is_active(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Starting | Self::Running | Self::Reconnecting | Self::Stopping
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ProxyRemoteState {
|
||||
gen: u64,
|
||||
by_name: HashMap<String, String>,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
status: Mutex<ClientStatus>,
|
||||
last_error: Mutex<Option<String>>,
|
||||
pending_logs: Mutex<Vec<String>>,
|
||||
proxy_remotes: Mutex<ProxyRemoteState>,
|
||||
cancel: Mutex<Option<CancellationToken>>,
|
||||
join: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClientHandle {
|
||||
inner: Arc<Inner>,
|
||||
}
|
||||
|
||||
impl Default for ClientHandle {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientHandle {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Inner {
|
||||
status: Mutex::new(ClientStatus::Stopped),
|
||||
last_error: Mutex::new(None),
|
||||
pending_logs: Mutex::new(Vec::new()),
|
||||
proxy_remotes: Mutex::new(ProxyRemoteState::default()),
|
||||
cancel: Mutex::new(None),
|
||||
join: Mutex::new(None),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> ClientStatus {
|
||||
*self.inner.status.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
pub fn last_error(&self) -> Option<String> {
|
||||
self.inner
|
||||
.last_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn push_log(&self, line: impl Into<String>) {
|
||||
self.enqueue_log(line.into());
|
||||
}
|
||||
|
||||
pub fn drain_logs(&self) -> Vec<String> {
|
||||
std::mem::take(
|
||||
&mut *self
|
||||
.inner
|
||||
.pending_logs
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn proxy_remotes_if_changed(
|
||||
&self,
|
||||
since_gen: u64,
|
||||
) -> Option<(u64, HashMap<String, String>)> {
|
||||
let g = self
|
||||
.inner
|
||||
.proxy_remotes
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if g.gen == since_gen {
|
||||
return None;
|
||||
}
|
||||
Some((g.gen, g.by_name.clone()))
|
||||
}
|
||||
|
||||
pub fn clear_proxy_remotes(&self) {
|
||||
let mut g = self
|
||||
.inner
|
||||
.proxy_remotes
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
g.by_name.clear();
|
||||
g.gen = g.gen.wrapping_add(1);
|
||||
}
|
||||
|
||||
fn set_proxy_remote(&self, name: String, remote_addr: String) {
|
||||
if name.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut g = self
|
||||
.inner
|
||||
.proxy_remotes
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if g.by_name.get(&name) == Some(&remote_addr) {
|
||||
return;
|
||||
}
|
||||
g.by_name.insert(name, remote_addr);
|
||||
g.gen = g.gen.wrapping_add(1);
|
||||
}
|
||||
|
||||
fn enqueue_log(&self, line: String) {
|
||||
if let Ok(mut g) = self.inner.pending_logs.lock() {
|
||||
const MAX_PENDING: usize = 500;
|
||||
if g.len() >= MAX_PENDING {
|
||||
let drop_n = g.len() - MAX_PENDING + 1;
|
||||
g.drain(0..drop_n);
|
||||
}
|
||||
g.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_status(&self, s: ClientStatus) {
|
||||
if let Ok(mut g) = self.inner.status.lock() {
|
||||
*g = s;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_error(&self, e: Option<String>) {
|
||||
if let Ok(mut g) = self.inner.last_error.lock() {
|
||||
*g = e;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_foreground(self, cfg: ClientConfig, config_path: PathBuf) -> Result<()> {
|
||||
let cancel = CancellationToken::new();
|
||||
self.set_status(ClientStatus::Starting);
|
||||
self.set_error(None);
|
||||
self.clear_proxy_remotes();
|
||||
let result = Service::new(cfg, config_path)
|
||||
.run(
|
||||
cancel.clone(),
|
||||
{
|
||||
let h = self.clone();
|
||||
move |st| h.set_status(st)
|
||||
},
|
||||
{
|
||||
let h = self.clone();
|
||||
move |line| {
|
||||
h.enqueue_log(line);
|
||||
}
|
||||
},
|
||||
{
|
||||
let h = self.clone();
|
||||
Arc::new(move |name, remote| h.set_proxy_remote(name, remote))
|
||||
},
|
||||
{
|
||||
let h = self.clone();
|
||||
Arc::new(move || h.clear_proxy_remotes())
|
||||
},
|
||||
)
|
||||
.await;
|
||||
self.clear_proxy_remotes();
|
||||
self.set_status(ClientStatus::Stopped);
|
||||
if let Err(ref e) = result {
|
||||
self.set_error(Some(e.to_string()));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn start(&self, cfg: ClientConfig, config_path: PathBuf) -> Result<()> {
|
||||
if self.status().is_active() {
|
||||
bail!("client already running");
|
||||
}
|
||||
let cancel = CancellationToken::new();
|
||||
*self.inner.cancel.lock().unwrap_or_else(|e| e.into_inner()) = Some(cancel.clone());
|
||||
self.set_status(ClientStatus::Starting);
|
||||
self.set_error(None);
|
||||
self.clear_proxy_remotes();
|
||||
|
||||
let handle = self.clone();
|
||||
let join = tokio::spawn(async move {
|
||||
let on_status = {
|
||||
let h = handle.clone();
|
||||
move |st| h.set_status(st)
|
||||
};
|
||||
let on_log = {
|
||||
let h = handle.clone();
|
||||
move |line: String| {
|
||||
h.enqueue_log(line);
|
||||
}
|
||||
};
|
||||
let on_proxy_remote: Arc<dyn Fn(String, String) + Send + Sync> = {
|
||||
let h = handle.clone();
|
||||
Arc::new(move |name: String, remote: String| h.set_proxy_remote(name, remote))
|
||||
};
|
||||
let on_remotes_clear: Arc<dyn Fn() + Send + Sync> = {
|
||||
let h = handle.clone();
|
||||
Arc::new(move || h.clear_proxy_remotes())
|
||||
};
|
||||
let result = Service::new(cfg, config_path)
|
||||
.run(cancel, on_status, on_log, on_proxy_remote, on_remotes_clear)
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
tracing::error!(error = %e, "client service ended with error");
|
||||
handle.set_error(Some(e.to_string()));
|
||||
}
|
||||
handle.clear_proxy_remotes();
|
||||
handle.set_status(ClientStatus::Stopped);
|
||||
*handle
|
||||
.inner
|
||||
.cancel
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = None;
|
||||
});
|
||||
*self.inner.join.lock().unwrap_or_else(|e| e.into_inner()) = Some(join);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(&self) {
|
||||
if matches!(self.status(), ClientStatus::Stopped) {
|
||||
return;
|
||||
}
|
||||
self.set_status(ClientStatus::Stopping);
|
||||
if let Some(token) = self
|
||||
.inner
|
||||
.cancel
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take()
|
||||
{
|
||||
token.cancel();
|
||||
}
|
||||
let join = self
|
||||
.inner
|
||||
.join
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take();
|
||||
if let Some(join) = join {
|
||||
let abort = join.abort_handle();
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), join).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => tracing::warn!(error = %e, "client task join error"),
|
||||
Err(_) => {
|
||||
tracing::warn!("client stop timed out after 5s — aborting task");
|
||||
abort.abort();
|
||||
self.set_status(ClientStatus::Stopped);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.set_status(ClientStatus::Stopped);
|
||||
}
|
||||
self.clear_proxy_remotes();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mod connector;
|
||||
mod control;
|
||||
mod handle;
|
||||
mod plugin;
|
||||
mod proxy;
|
||||
mod run_id;
|
||||
mod service;
|
||||
|
||||
pub use handle::{ClientHandle, ClientStatus};
|
||||
pub use orbien_core::config::{resolve_client_config_path, ClientConfig};
|
||||
pub use service::Service;
|
||||
|
||||
pub use orbien_core::VERSION;
|
||||
+6
-10
@@ -1,12 +1,6 @@
|
||||
mod connector;
|
||||
mod control;
|
||||
mod plugin;
|
||||
mod proxy;
|
||||
mod run_id;
|
||||
mod service;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use orbien_client::ClientHandle;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -29,10 +23,10 @@ async fn main() -> Result<()> {
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
let config_path = orbien_core::config::resolve_client_config_path(args.config.as_deref())?;
|
||||
let config_path = orbien_client::resolve_client_config_path(args.config.as_deref())?;
|
||||
tracing::info!(config = %config_path.display(), "loading config");
|
||||
|
||||
let cfg = orbien_core::config::ClientConfig::load(&config_path)?;
|
||||
let cfg = orbien_client::ClientConfig::load(&config_path)?;
|
||||
tracing::info!(
|
||||
server = %cfg.server_endpoint(),
|
||||
protocol = %cfg.transport.protocol,
|
||||
@@ -40,5 +34,7 @@ async fn main() -> Result<()> {
|
||||
"starting orbien"
|
||||
);
|
||||
|
||||
service::Service::new(cfg, &config_path).run().await
|
||||
ClientHandle::new()
|
||||
.run_foreground(cfg, config_path)
|
||||
.await
|
||||
}
|
||||
|
||||
+69
-8
@@ -1,10 +1,13 @@
|
||||
use crate::control::{Control, SessionEnd};
|
||||
use crate::handle::ClientStatus;
|
||||
use crate::run_id;
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::config::ClientConfig;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub struct Service {
|
||||
cfg: ClientConfig,
|
||||
@@ -19,14 +22,51 @@ impl Service {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(self) -> Result<()> {
|
||||
pub async fn run(
|
||||
self,
|
||||
cancel: CancellationToken,
|
||||
mut on_status: impl FnMut(ClientStatus),
|
||||
mut on_log: impl FnMut(String),
|
||||
on_proxy_remote: Arc<dyn Fn(String, String) + Send + Sync>,
|
||||
on_remotes_clear: Arc<dyn Fn() + Send + Sync>,
|
||||
) -> Result<()> {
|
||||
let mut run_id = run_id::load(&self.config_path);
|
||||
if !run_id.is_empty() {
|
||||
tracing::info!(%run_id, "restored persisted run_id");
|
||||
}
|
||||
|
||||
let mut first_attempt = true;
|
||||
loop {
|
||||
match Control::start(&self.cfg, run_id.clone(), &self.config_path).await {
|
||||
if cancel.is_cancelled() {
|
||||
tracing::info!("client service cancelled");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
on_remotes_clear();
|
||||
|
||||
if first_attempt {
|
||||
on_status(ClientStatus::Starting);
|
||||
on_log("INFO connecting to server".into());
|
||||
} else {
|
||||
on_status(ClientStatus::Reconnecting);
|
||||
}
|
||||
|
||||
let end = Control::start(
|
||||
&self.cfg,
|
||||
run_id.clone(),
|
||||
&self.config_path,
|
||||
cancel.clone(),
|
||||
|| {
|
||||
on_status(ClientStatus::Running);
|
||||
on_log("INFO connected to server".into());
|
||||
},
|
||||
Arc::clone(&on_proxy_remote),
|
||||
)
|
||||
.await;
|
||||
|
||||
on_remotes_clear();
|
||||
|
||||
match end {
|
||||
Ok(SessionEnd::Kicked {
|
||||
run_id: rid,
|
||||
reason,
|
||||
@@ -34,19 +74,40 @@ impl Service {
|
||||
tracing::error!(
|
||||
run_id = %rid,
|
||||
%reason,
|
||||
"kicked by server — process will exit (no reconnect)"
|
||||
"kicked by server — stopping (no reconnect)"
|
||||
);
|
||||
return Ok(());
|
||||
on_log(format!("ERROR kicked by server: {reason}"));
|
||||
return Err(anyhow!("kicked by server: {reason}"));
|
||||
}
|
||||
Ok(SessionEnd::Disconnected { run_id: rid }) => {
|
||||
if cancel.is_cancelled() {
|
||||
tracing::info!(run_id = %rid, "session ended after cancel");
|
||||
return Ok(());
|
||||
}
|
||||
run_id = rid;
|
||||
tracing::warn!("control session ended, reconnecting in 3s...");
|
||||
on_log("WARN disconnected from server".into());
|
||||
on_status(ClientStatus::Reconnecting);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "failed to establish control, retry in 3s");
|
||||
if cancel.is_cancelled() {
|
||||
tracing::info!("session error after cancel: {e}");
|
||||
return Ok(());
|
||||
}
|
||||
on_log(format!("ERROR failed to connect: {e}"));
|
||||
on_status(ClientStatus::Reconnecting);
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
|
||||
first_attempt = false;
|
||||
on_log("INFO retrying in 3s".into());
|
||||
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => {
|
||||
tracing::info!("client service cancelled during backoff");
|
||||
return Ok(());
|
||||
}
|
||||
_ = sleep(Duration::from_secs(3)) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
BIN_DIR="$ROOT/desktop/src-tauri/binaries"
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
TARGET="${1:-}"
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
TARGET="$(rustc --print host-tuple 2>/dev/null || rustc -Vv | awk '/^host:/{print $2}')"
|
||||
fi
|
||||
|
||||
EXT=""
|
||||
case "$TARGET" in
|
||||
*windows*) EXT=".exe" ;;
|
||||
esac
|
||||
|
||||
LOCKED_ARGS=()
|
||||
if [[ "${CI:-}" == "true" || "${CARGO_LOCKED:-}" == "1" ]]; then
|
||||
LOCKED_ARGS+=(--locked)
|
||||
fi
|
||||
|
||||
HOST="$(rustc --print host-tuple 2>/dev/null || rustc -Vv | awk '/^host:/{print $2}')"
|
||||
echo "building orbien sidecar for target=${TARGET} (host=${HOST})"
|
||||
|
||||
build_sidecar() {
|
||||
if [[ "$TARGET" == "$HOST" ]]; then
|
||||
cargo build --release ${LOCKED_ARGS[@]+"${LOCKED_ARGS[@]}"} \
|
||||
-p orbien-client --manifest-path "$ROOT/Cargo.toml"
|
||||
SRC="$ROOT/target/release/orbien${EXT}"
|
||||
else
|
||||
cargo build --release ${LOCKED_ARGS[@]+"${LOCKED_ARGS[@]}"} \
|
||||
-p orbien-client --target "$TARGET" --manifest-path "$ROOT/Cargo.toml"
|
||||
SRC="$ROOT/target/${TARGET}/release/orbien${EXT}"
|
||||
fi
|
||||
}
|
||||
build_sidecar
|
||||
|
||||
if [[ ! -f "$SRC" ]]; then
|
||||
echo "sidecar binary not found: $SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEST="$BIN_DIR/orbien-${TARGET}${EXT}"
|
||||
cp "$SRC" "$DEST"
|
||||
chmod +x "$DEST" 2>/dev/null || true
|
||||
ls -lh "$DEST"
|
||||
echo "sidecar ready: $DEST"
|
||||
Reference in New Issue
Block a user