From 7bd37b3c4ad5fdf97f074fb24af78cb2dc783233 Mon Sep 17 00:00:00 2001 From: hdbg Date: Wed, 25 Feb 2026 18:57:16 +0100 Subject: [PATCH] refactor: introduce `TransportActor` abstraction --- app/.dart_tool/package_config.json | 178 ++++++++++++++ app/.dart_tool/package_graph.json | 230 ++++++++++++++++++ app/.dart_tool/version | 1 + .../ephemeral/Flutter-Generated.xcconfig | 11 + .../ephemeral/flutter_export_environment.sh | 12 + server/Cargo.lock | 1 + server/crates/arbiter-proto/Cargo.toml | 1 + server/crates/arbiter-proto/src/transport.rs | 146 ++++++++++- server/crates/arbiter-useragent/Cargo.toml | 2 +- server/crates/arbiter-useragent/src/lib.rs | 62 ++++- 10 files changed, 637 insertions(+), 7 deletions(-) create mode 100644 app/.dart_tool/package_config.json create mode 100644 app/.dart_tool/package_graph.json create mode 100644 app/.dart_tool/version create mode 100644 app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig create mode 100755 app/macos/Flutter/ephemeral/flutter_export_environment.sh diff --git a/app/.dart_tool/package_config.json b/app/.dart_tool/package_config.json new file mode 100644 index 0000000..258edaf --- /dev/null +++ b/app/.dart_tool/package_config.json @@ -0,0 +1,178 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "async", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/async-2.13.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "boolean_selector", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "characters", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/characters-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "clock", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/clock-1.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "collection", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/collection-1.19.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "cupertino_icons", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/cupertino_icons-1.0.8", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "fake_async", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/fake_async-1.3.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "flutter", + "rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/packages/flutter", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "flutter_lints", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/flutter_lints-6.0.0", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "flutter_test", + "rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/packages/flutter_test", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "leak_tracker", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker-11.0.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "leak_tracker_flutter_testing", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "leak_tracker_testing", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "lints", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/lints-6.1.0", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "matcher", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/matcher-0.12.17", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "material_color_utilities", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "meta", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/meta-1.17.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "path", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/path-1.9.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "sky_engine", + "rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/bin/cache/pkg/sky_engine", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "source_span", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/source_span-1.10.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "stack_trace", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/stack_trace-1.12.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "stream_channel", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/stream_channel-2.1.4", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "string_scanner", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/string_scanner-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "term_glyph", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/term_glyph-1.2.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "test_api", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/test_api-0.7.7", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "vector_math", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/vector_math-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "vm_service", + "rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/vm_service-15.0.2", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "app", + "rootUri": "../", + "packageUri": "lib/", + "languageVersion": "3.10" + } + ], + "generator": "pub", + "generatorVersion": "3.10.8", + "flutterRoot": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable", + "flutterVersion": "3.38.9", + "pubCache": "file:///Users/kaska/.pub-cache" +} diff --git a/app/.dart_tool/package_graph.json b/app/.dart_tool/package_graph.json new file mode 100644 index 0000000..2affe54 --- /dev/null +++ b/app/.dart_tool/package_graph.json @@ -0,0 +1,230 @@ +{ + "roots": [ + "app" + ], + "packages": [ + { + "name": "app", + "version": "1.0.0+1", + "dependencies": [ + "cupertino_icons", + "flutter" + ], + "devDependencies": [ + "flutter_lints", + "flutter_test" + ] + }, + { + "name": "flutter_lints", + "version": "6.0.0", + "dependencies": [ + "lints" + ] + }, + { + "name": "flutter_test", + "version": "0.0.0", + "dependencies": [ + "clock", + "collection", + "fake_async", + "flutter", + "leak_tracker_flutter_testing", + "matcher", + "meta", + "path", + "stack_trace", + "stream_channel", + "test_api", + "vector_math" + ] + }, + { + "name": "cupertino_icons", + "version": "1.0.8", + "dependencies": [] + }, + { + "name": "flutter", + "version": "0.0.0", + "dependencies": [ + "characters", + "collection", + "material_color_utilities", + "meta", + "sky_engine", + "vector_math" + ] + }, + { + "name": "lints", + "version": "6.1.0", + "dependencies": [] + }, + { + "name": "stream_channel", + "version": "2.1.4", + "dependencies": [ + "async" + ] + }, + { + "name": "meta", + "version": "1.17.0", + "dependencies": [] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "leak_tracker_flutter_testing", + "version": "3.0.10", + "dependencies": [ + "flutter", + "leak_tracker", + "leak_tracker_testing", + "matcher", + "meta" + ] + }, + { + "name": "vector_math", + "version": "2.2.0", + "dependencies": [] + }, + { + "name": "stack_trace", + "version": "1.12.1", + "dependencies": [ + "path" + ] + }, + { + "name": "clock", + "version": "1.1.2", + "dependencies": [] + }, + { + "name": "fake_async", + "version": "1.3.3", + "dependencies": [ + "clock", + "collection" + ] + }, + { + "name": "path", + "version": "1.9.1", + "dependencies": [] + }, + { + "name": "matcher", + "version": "0.12.17", + "dependencies": [ + "async", + "meta", + "stack_trace", + "term_glyph", + "test_api" + ] + }, + { + "name": "test_api", + "version": "0.7.7", + "dependencies": [ + "async", + "boolean_selector", + "collection", + "meta", + "source_span", + "stack_trace", + "stream_channel", + "string_scanner", + "term_glyph" + ] + }, + { + "name": "sky_engine", + "version": "0.0.0", + "dependencies": [] + }, + { + "name": "material_color_utilities", + "version": "0.11.1", + "dependencies": [ + "collection" + ] + }, + { + "name": "characters", + "version": "1.4.0", + "dependencies": [] + }, + { + "name": "async", + "version": "2.13.0", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "leak_tracker_testing", + "version": "3.0.2", + "dependencies": [ + "leak_tracker", + "matcher", + "meta" + ] + }, + { + "name": "leak_tracker", + "version": "11.0.2", + "dependencies": [ + "clock", + "collection", + "meta", + "path", + "vm_service" + ] + }, + { + "name": "term_glyph", + "version": "1.2.2", + "dependencies": [] + }, + { + "name": "string_scanner", + "version": "1.4.1", + "dependencies": [ + "source_span" + ] + }, + { + "name": "source_span", + "version": "1.10.2", + "dependencies": [ + "collection", + "path", + "term_glyph" + ] + }, + { + "name": "boolean_selector", + "version": "2.1.2", + "dependencies": [ + "source_span", + "string_scanner" + ] + }, + { + "name": "vm_service", + "version": "15.0.2", + "dependencies": [] + } + ], + "configVersion": 1 +} \ No newline at end of file diff --git a/app/.dart_tool/version b/app/.dart_tool/version new file mode 100644 index 0000000..75fffa6 --- /dev/null +++ b/app/.dart_tool/version @@ -0,0 +1 @@ +3.38.9 \ No newline at end of file diff --git a/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig new file mode 100644 index 0000000..6da0bad --- /dev/null +++ b/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -0,0 +1,11 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable +FLUTTER_APPLICATION_PATH=/Users/kaska/Documents/Projects/Major/arbiter/app +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NUMBER=1 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/app/macos/Flutter/ephemeral/flutter_export_environment.sh b/app/macos/Flutter/ephemeral/flutter_export_environment.sh new file mode 100755 index 0000000..1062508 --- /dev/null +++ b/app/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable" +export "FLUTTER_APPLICATION_PATH=/Users/kaska/Documents/Projects/Major/arbiter/app" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/server/Cargo.lock b/server/Cargo.lock index 3e8ae9e..b7e6b44 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -73,6 +73,7 @@ dependencies = [ "tonic", "tonic-prost", "tonic-prost-build", + "tracing", "url", ] diff --git a/server/crates/arbiter-proto/Cargo.toml b/server/crates/arbiter-proto/Cargo.toml index 60d27cf..dae0fab 100644 --- a/server/crates/arbiter-proto/Cargo.toml +++ b/server/crates/arbiter-proto/Cargo.toml @@ -17,6 +17,7 @@ miette.workspace = true thiserror.workspace = true rustls-pki-types.workspace = true base64 = "0.22.1" +tracing.workspace = true [build-dependencies] diff --git a/server/crates/arbiter-proto/src/transport.rs b/server/crates/arbiter-proto/src/transport.rs index 691ef9a..02e54c5 100644 --- a/server/crates/arbiter-proto/src/transport.rs +++ b/server/crates/arbiter-proto/src/transport.rs @@ -1,7 +1,16 @@ use futures::{Stream, StreamExt}; -use tokio::sync::mpsc::{self, error::SendError}; +use kameo::{ + Actor, + actor::{ActorRef, PreparedActor, Spawn, WeakActorRef}, + mailbox::Signal, + prelude::Message, +}; +use tokio::{ + select, + sync::mpsc::{self, error::SendError}, +}; use tonic::{Status, Streaming}; - +use tracing::{debug, error}; // Abstraction for stream for sans-io capabilities pub trait Bi: Stream> + Send + Sync + 'static { @@ -44,3 +53,136 @@ where self.response_sender.send(item).await } } + +pub trait TransportActor: Actor + Send + Message {} + +pub struct GrpcTransportActor +where + SendMsg: Send + 'static, + RecvMsg: Send + 'static, + A: TransportActor, +{ + pub sender: mpsc::Sender>, + pub receiver: tonic::Streaming, + pub business_logic_actor: ActorRef, +} +impl Actor for GrpcTransportActor +where + SendMsg: Send + 'static, + RecvMsg: Send + 'static, + A: TransportActor, +{ + type Args = Self; + + type Error = (); + + async fn on_start(args: Self::Args, _: ActorRef) -> Result { + Ok(args) + } + + fn on_link_died( + &mut self, + _: WeakActorRef, + id: kameo::prelude::ActorId, + _: kameo::prelude::ActorStopReason, + ) -> impl Future< + Output = Result, Self::Error>, + > + Send { + async move { + if id == self.business_logic_actor.id() { + error!("Business logic actor died, stopping GrpcTransportActor"); + Ok(std::ops::ControlFlow::Break( + kameo::prelude::ActorStopReason::Normal, + )) + } else { + debug!( + "Linked actor {} died, but it's not the business logic actor, ignoring", + id + ); + Ok(std::ops::ControlFlow::Continue(())) + } + } + } + + async fn next( + &mut self, + _: WeakActorRef, + mailbox_rx: &mut kameo::prelude::MailboxReceiver, + ) -> Option> { + select! { + msg = mailbox_rx.recv() => { + msg + } + recv_msg = self.receiver.next() => { + match recv_msg { + Some(Ok(msg)) => { + match self.business_logic_actor.tell(msg).await { + Ok(_) => None, + Err(e) => { + // TODO: this would probably require better error handling - or resending if backpressure is the issue + error!("Failed to send message to business logic actor: {}", e); + Some(Signal::Stop) + } + } + } + Some(Err(e)) => { + error!("Received error from stream: {}, stopping GrpcTransportActor", e); + Some(Signal::Stop) + } + None => { + error!("Receiver channel closed, stopping GrpcTransportActor"); + Some(Signal::Stop) + } + } + } + } + } +} + +impl> Message + for GrpcTransportActor +{ + type Reply = (); + + async fn handle( + &mut self, + msg: SendMsg, + ctx: &mut kameo::prelude::Context, + ) -> Self::Reply { + let err = self.sender.send(Ok(msg)).await; + match err { + Ok(_) => (), + Err(e) => { + error!("Failed to send message: {}", e); + ctx.stop(); + } + } + } +} + +pub async fn wire( + business_ctor: BusinessCtor, + transport_ctor: TransportCtor, +) -> (ActorRef, ActorRef) +where + T: TransportActor, + RecvMsg: Send + 'static, + SendMsg: Send + 'static, + BusinessActor: Actor + Send + 'static, + BusinessCtor: FnOnce(ActorRef) -> BusinessActor::Args, + TransportCtor: FnOnce(ActorRef) -> T::Args, +{ + let prepared_business: PreparedActor = Spawn::prepare(); + let prepared_transport: PreparedActor = Spawn::prepare(); + + let business_ref = prepared_business.actor_ref().clone(); + let transport_ref = prepared_transport.actor_ref().clone(); + + transport_ref.link(&business_ref).await; + business_ref.link(&transport_ref).await; + + let _ = prepared_business.spawn(business_ctor(transport_ref.clone())); + let _ = prepared_transport.spawn(transport_ctor(business_ref.clone())); + + (transport_ref, business_ref) +} diff --git a/server/crates/arbiter-useragent/Cargo.toml b/server/crates/arbiter-useragent/Cargo.toml index 16eb12d..1a966c7 100644 --- a/server/crates/arbiter-useragent/Cargo.toml +++ b/server/crates/arbiter-useragent/Cargo.toml @@ -12,4 +12,4 @@ tonic.workspace = true tracing.workspace = true ed25519-dalek.workspace = true smlang.workspace = true -x25519-dalek.workspace = true \ No newline at end of file +x25519-dalek.workspace = true diff --git a/server/crates/arbiter-useragent/src/lib.rs b/server/crates/arbiter-useragent/src/lib.rs index c4da0d0..9d35ddf 100644 --- a/server/crates/arbiter-useragent/src/lib.rs +++ b/server/crates/arbiter-useragent/src/lib.rs @@ -1,13 +1,67 @@ +use arbiter_proto::proto::UserAgentRequest; use ed25519_dalek::SigningKey; -use kameo::Actor; +use kameo::{ + Actor, Reply, + actor::{ActorRef, WeakActorRef}, + prelude::Message, +}; +use smlang::statemachine; +use tokio::sync::mpsc; use tonic::transport::CertificateDer; +use tracing::{debug, error}; struct Storage { pub identity: SigningKey, pub server_ca_cert: CertificateDer<'static>, } -#[derive(Actor)] -pub struct UserAgent { +#[derive(Debug)] +pub enum InitError { + StorageError, + Other(String), +} -} \ No newline at end of file +statemachine! { + name: UserAgentStateMachine, + custom_error: false, + transitions: { + *Init + SendAuthChallenge = WaitingForAuthSolution + } +} + + + +pub struct UserAgentActor> { + key: SigningKey, + server_ca_cert: CertificateDer<'static>, + sender: ActorRef, +} +impl> Actor for UserAgentActor { + type Args = Self; + + type Error = InitError; + + async fn on_start(args: Self::Args, actor_ref: ActorRef) -> Result { + todo!() + } + + async fn on_link_died( + &mut self, + _: WeakActorRef, + id: kameo::prelude::ActorId, + _: kameo::prelude::ActorStopReason, + ) -> Result, Self::Error> { + if id == self.sender.id() { + error!("Transport actor died, stopping UserAgentActor"); + Ok(std::ops::ControlFlow::Break( + kameo::prelude::ActorStopReason::Normal, + )) + } else { + debug!( + "Linked actor {} died, but it's not the transport actor, ignoring", + id + ); + Ok(std::ops::ControlFlow::Continue(())) + } + } +}