From 47f581cc1d221a0842b188a080f0f6bd6874e3b4 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 10 May 2026 12:40:38 -0400 Subject: [PATCH] fix(server): improve sandbox service discovery Fall back to procfs when ss is unavailable, report the discovery source in API metadata, and surface the sandbox install tip in the services UI. Previewable services are ordered first for clearer service selection. --- .../run-sandbox/services-panel.test.tsx | 63 ++++- .../app/routes/run-sandbox/services-panel.tsx | 60 +++-- docs/public/api-reference/fabro-api.yaml | 23 +- lib/crates/fabro-api/build.rs | 10 + .../tests/sandbox_services_round_trip.rs | 19 +- lib/crates/fabro-server/src/demo/mod.rs | 4 + .../src/server/handler/sandbox.rs | 235 +++++++++++++++--- lib/crates/fabro-types/src/lib.rs | 5 +- .../fabro-types/src/sandbox_services.rs | 13 + .../src/.openapi-generator/FILES | 2 + .../fabro-api-client/src/models/index.ts | 2 + .../sandbox-service-discovery-source.ts | 29 +++ .../src/models/sandbox-service-list-meta.ts | 28 +++ .../models/sandbox-service-list-response.ts | 4 + .../src/models/sandbox-service.ts | 4 +- 15 files changed, 432 insertions(+), 69 deletions(-) create mode 100644 lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts create mode 100644 lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts diff --git a/apps/fabro-web/app/routes/run-sandbox/services-panel.test.tsx b/apps/fabro-web/app/routes/run-sandbox/services-panel.test.tsx index d4083edd7..38403196e 100644 --- a/apps/fabro-web/app/routes/run-sandbox/services-panel.test.tsx +++ b/apps/fabro-web/app/routes/run-sandbox/services-panel.test.tsx @@ -25,6 +25,13 @@ function makeIdlePreview(): PreviewMutationShape { }; } +function makeServicesData(data: SandboxService[]) { + return { + data, + meta: { source: "ss" as const }, + }; +} + const mountedRenderers: TestRenderer.ReactTestRenderer[] = []; interface WindowLike { @@ -97,7 +104,7 @@ describe("ServicesPanelView", () => { test("shows empty state when the services list is empty", () => { const renderer = renderView({ - servicesQuery: { ...makeIdleQuery(), data: { data: [] } }, + servicesQuery: { ...makeIdleQuery(), data: makeServicesData([]) }, previewMutation: makeIdlePreview(), }); const titles = renderer.root.findAll( @@ -109,6 +116,46 @@ describe("ServicesPanelView", () => { expect(titles).toHaveLength(1); }); + test("shows an iproute2 tip when services were discovered from procfs", () => { + const service: SandboxService = { + port: 3000, + addresses: ["0.0.0.0:3000"], + processes: [], + preview_supported: true, + }; + const renderer = renderView({ + servicesQuery: { + ...makeIdleQuery(), + data: { + data: [service], + meta: { source: "procfs" }, + }, + }, + previewMutation: makeIdlePreview(), + }); + + const tipLabels = renderer.root.findAll( + (node) => + node.type === "span" && + Array.isArray(node.children) && + node.children.includes("Tip:"), + ); + expect(tipLabels).toHaveLength(1); + + const commands = renderer.root.findAll( + (node) => + node.type === "code" && + Array.isArray(node.children) && + node.children.includes("apt-get install iproute2"), + ); + expect(commands).toHaveLength(1); + + const tipText = JSON.stringify(renderer.toJSON()); + expect(tipText).toContain("Install "); + expect(tipText).toContain("ss"); + expect(tipText).toContain(" in the sandbox for improved services listing:"); + }); + test("shows API error state with the error message", () => { const renderer = renderView({ servicesQuery: { @@ -139,7 +186,7 @@ describe("ServicesPanelView", () => { preview_supported: false, }; const renderer = renderView({ - servicesQuery: { ...makeIdleQuery(), data: { data: [service] } }, + servicesQuery: { ...makeIdleQuery(), data: makeServicesData([service]) }, previewMutation: makeIdlePreview(), }); @@ -158,14 +205,6 @@ describe("ServicesPanelView", () => { node.children.includes("Preview"), ); expect(previewButtons).toHaveLength(0); - - const unavailable = renderer.root.findAll( - (node) => - node.type === "span" && - Array.isArray(node.children) && - node.children.includes("Unavailable"), - ); - expect(unavailable).toHaveLength(1); }); test("renders a Preview button for a previewable service and triggers with signed args", async () => { @@ -186,7 +225,7 @@ describe("ServicesPanelView", () => { installWindowOpen(windowOpenSpy); const renderer = renderView({ - servicesQuery: { ...makeIdleQuery(), data: { data: [service] } }, + servicesQuery: { ...makeIdleQuery(), data: makeServicesData([service]) }, previewMutation: { trigger }, }); const previewButton = renderer.root.find( @@ -226,7 +265,7 @@ describe("ServicesPanelView", () => { installWindowOpen(windowOpenSpy); const renderer = renderView({ - servicesQuery: { ...makeIdleQuery(), data: { data: [service] } }, + servicesQuery: { ...makeIdleQuery(), data: makeServicesData([service]) }, previewMutation: { trigger }, }); const previewButton = renderer.root.find( diff --git a/apps/fabro-web/app/routes/run-sandbox/services-panel.tsx b/apps/fabro-web/app/routes/run-sandbox/services-panel.tsx index 267e979b1..78a17a63a 100644 --- a/apps/fabro-web/app/routes/run-sandbox/services-panel.tsx +++ b/apps/fabro-web/app/routes/run-sandbox/services-panel.tsx @@ -1,6 +1,9 @@ import { useState } from "react"; import { ArrowPathIcon } from "@heroicons/react/20/solid"; -import type { SandboxService } from "@qltysh/fabro-api-client"; +import type { + SandboxService, + SandboxServiceListResponse, +} from "@qltysh/fabro-api-client"; import { useSandboxServices } from "../../lib/queries"; import { usePreviewRun } from "../../lib/mutations"; @@ -10,7 +13,7 @@ import { ErrorState, LoadingState, } from "../../components/state"; -import { SECONDARY_BUTTON_CLASS, Tooltip } from "../../components/ui"; +import { Tooltip } from "../../components/ui"; export interface ServicesPanelProps { runId: string; @@ -20,6 +23,9 @@ export interface ServicesPanelProps { const REFRESH_BUTTON_CLASS = "inline-flex size-9 items-center justify-center rounded-lg text-fg-2 outline-1 -outline-offset-1 outline-white/10 transition-colors hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-50"; +const PREVIEW_BUTTON_CLASS = + "inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium text-fg-2 outline-1 -outline-offset-1 outline-white/10 transition-colors hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-60"; + export default function ServicesPanel({ runId, leading }: ServicesPanelProps) { const servicesQuery = useSandboxServices(runId); const previewMutation = usePreviewRun(runId); @@ -34,7 +40,7 @@ export default function ServicesPanel({ runId, leading }: ServicesPanelProps) { } export interface ServicesQueryShape { - data?: { data: SandboxService[] } | undefined; + data?: SandboxServiceListResponse | undefined; error?: unknown; isLoading: boolean; isValidating: boolean; @@ -71,6 +77,7 @@ export function ServicesPanelView({ const [previewError, setPreviewError] = useState(null); const services = servicesQuery.data?.data ?? []; + const discoverySource = servicesQuery.data?.meta.source; const queryErrorMessage = describeQueryError(servicesQuery.error); const showLoading = servicesQuery.isLoading && !servicesQuery.data; const showError = queryErrorMessage !== null && !servicesQuery.data; @@ -142,11 +149,14 @@ export function ServicesPanelView({ ) : services.length === 0 ? ( ) : ( - + <> + {discoverySource === "procfs" ? : null} + + )} @@ -160,6 +170,17 @@ function describeQueryError(error: unknown): string | null { return "Could not load services."; } +function ProcfsDiscoveryTip() { + return ( +
+ Tip:{" "} + Install ss in the sandbox + for improved services listing:{" "} + apt-get install iproute2 +
+ ); +} + function ServicesTable({ services, pendingPort, @@ -174,10 +195,11 @@ function ServicesTable({ - - - - + + + + @@ -206,34 +228,32 @@ function ServiceRow({ }) { return ( - - + - - ); diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 4971f3f8b..300253a3f 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -8004,13 +8004,13 @@ components: example: 3000 addresses: type: array - description: Local bind addresses reported by `ss`. + description: Local bind addresses discovered from `ss` or `/proc/net/tcp*`. items: type: string example: ["127.0.0.1:3000", "[::]:3000"] processes: type: array - description: Visible process summaries reported by `ss`. + description: Visible process summaries when available. Empty when the sandbox only supports `/proc/net/tcp*` discovery. items: type: string example: ['users:(("node",pid=42,fd=23))'] @@ -8024,11 +8024,30 @@ components: type: object required: - data + - meta properties: data: type: array items: $ref: "#/components/schemas/SandboxService" + meta: + $ref: "#/components/schemas/SandboxServiceListMeta" + + SandboxServiceListMeta: + description: Metadata about sandbox service discovery. + type: object + required: + - source + properties: + source: + $ref: "#/components/schemas/SandboxServiceDiscoverySource" + + SandboxServiceDiscoverySource: + description: Tool or kernel interface used to discover sandbox services. + type: string + enum: + - ss + - procfs VncPreviewResponse: description: Response containing a signed noVNC preview URL for a Daytona sandbox. diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index f8896b9d6..6e0ce1ed4 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -386,6 +386,16 @@ fn main() { ("GitContext", "fabro_types::GitContext", &[]), ("SandboxDetails", "fabro_types::SandboxDetails", &[]), ("SandboxService", "fabro_types::SandboxService", &[]), + ( + "SandboxServiceDiscoverySource", + "fabro_types::SandboxServiceDiscoverySource", + &[], + ), + ( + "SandboxServiceListMeta", + "fabro_types::SandboxServiceListMeta", + &[], + ), ( "SandboxServiceListResponse", "fabro_types::SandboxServiceListResponse", diff --git a/lib/crates/fabro-api/tests/sandbox_services_round_trip.rs b/lib/crates/fabro-api/tests/sandbox_services_round_trip.rs index 32387fd2a..706ef59f2 100644 --- a/lib/crates/fabro-api/tests/sandbox_services_round_trip.rs +++ b/lib/crates/fabro-api/tests/sandbox_services_round_trip.rs @@ -4,7 +4,10 @@ use fabro_api::types::{ SandboxService as ApiSandboxService, SandboxServiceListResponse as ApiSandboxServiceListResponse, }; -use fabro_types::{SandboxService, SandboxServiceListResponse}; +use fabro_types::{ + SandboxService, SandboxServiceDiscoverySource, SandboxServiceListMeta, + SandboxServiceListResponse, +}; use serde_json::json; #[test] @@ -25,6 +28,9 @@ fn sandbox_services_json_matches_openapi_shape() { ], preview_supported: true, }], + meta: SandboxServiceListMeta { + source: SandboxServiceDiscoverySource::Ss, + }, }; assert_eq!( @@ -38,17 +44,22 @@ fn sandbox_services_json_matches_openapi_shape() { r#"users:(("vite",pid=84,fd=19))"#, ], "preview_supported": true - }] + }], + "meta": { + "source": "ss" + } }) ); } #[test] fn sandbox_services_deserializes_empty_response() { - let response: SandboxServiceListResponse = serde_json::from_value(json!({ "data": [] })) - .expect("empty service response should deserialize"); + let response: SandboxServiceListResponse = + serde_json::from_value(json!({ "data": [], "meta": { "source": "procfs" } })) + .expect("empty service response should deserialize"); assert!(response.data.is_empty()); + assert_eq!(response.meta.source, SandboxServiceDiscoverySource::Procfs); } fn assert_same_type() { diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 261302691..ce85d3807 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -23,6 +23,7 @@ use fabro_api::types::{ RunFilesMeta, RunFilesMetaScope, RunFilesMetaSource, SandboxService, SandboxServiceListResponse, }; +use fabro_types::{SandboxServiceDiscoverySource, SandboxServiceListMeta}; use serde_json::json; use crate::error::ApiError; @@ -423,6 +424,9 @@ pub(crate) async fn list_sandbox_services_stub( preview_supported: false, }, ], + meta: SandboxServiceListMeta { + source: SandboxServiceDiscoverySource::Ss, + }, }), ) .into_response() diff --git a/lib/crates/fabro-server/src/server/handler/sandbox.rs b/lib/crates/fabro-server/src/server/handler/sandbox.rs index 1f447cde5..329ca1d18 100644 --- a/lib/crates/fabro-server/src/server/handler/sandbox.rs +++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs @@ -1,9 +1,11 @@ use std::collections::BTreeMap; +use std::net::{Ipv4Addr, Ipv6Addr}; use std::num::NonZeroU64; use std::sync::Arc; use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}; use fabro_sandbox::{TerminalSize, open_terminal_for_run}; +use fabro_types::{SandboxServiceDiscoverySource, SandboxServiceListMeta}; use futures_util::FutureExt; use futures_util::future::BoxFuture; @@ -20,7 +22,19 @@ use super::super::{ const MAX_TERMINAL_CONTROL_BYTES: usize = 4096; const DEFAULT_VNC_NO_VNC_PORT: u16 = 6080; const DEFAULT_VNC_TTL_SECS: i32 = 3600; -const LIST_SANDBOX_SERVICES_COMMAND: &str = "ss -H -ltnp"; +const LIST_SANDBOX_SERVICES_COMMAND: &str = r#"if command -v ss >/dev/null 2>&1; then + ss -H -ltnp && exit 0 +fi +printf 'FABRO_PROC_NET_TCP procfs\n' +for file in /proc/net/tcp /proc/net/tcp6; do + if [ -r "$file" ]; then + printf 'FABRO_PROC_NET_TCP %s\n' "$file" + while IFS= read -r line; do + printf '%s\n' "$line" + done < "$file" + fi +done"#; +const LIST_SANDBOX_SERVICES_FAILURE_LABEL: &str = "sandbox service discovery command"; const LIST_SANDBOX_SERVICES_TIMEOUT_MS: u64 = 5_000; // Daytona's signed preview points at the noVNC service root, which serves a // directory listing. Force the iframe to the actual viewer page with @@ -572,8 +586,12 @@ async fn list_sandbox_services( .into_response(); } + let discovery = parse_sandbox_services(&result.stdout, &provider); Json(SandboxServiceListResponse { - data: parse_ss_listening_services(&result.stdout, &provider), + data: discovery.services, + meta: SandboxServiceListMeta { + source: discovery.source, + }, }) .into_response() } @@ -587,7 +605,29 @@ fn sandbox_service_command_failure_detail(result: &fabro_sandbox::ExecResult) -> if !stdout.is_empty() { return stdout.to_string(); } - format!("{LIST_SANDBOX_SERVICES_COMMAND} failed") + format!("{LIST_SANDBOX_SERVICES_FAILURE_LABEL} failed") +} + +struct SandboxServiceDiscovery { + services: Vec, + source: SandboxServiceDiscoverySource, +} + +fn parse_sandbox_services(output: &str, provider: &str) -> SandboxServiceDiscovery { + if output + .lines() + .any(|line| line.trim_start().starts_with("FABRO_PROC_NET_TCP ")) + { + SandboxServiceDiscovery { + services: parse_proc_net_listening_services(output, provider), + source: SandboxServiceDiscoverySource::Procfs, + } + } else { + SandboxServiceDiscovery { + services: parse_ss_listening_services(output, provider), + source: SandboxServiceDiscoverySource::Ss, + } + } } fn parse_ss_listening_services(output: &str, provider: &str) -> Vec { @@ -605,18 +645,9 @@ fn parse_ss_listening_services(output: &str, provider: &str) -> Vec 5).then(|| fields[5..].join(" ")); - let service = services.entry(port).or_insert_with(|| SandboxService { - port, - addresses: Vec::new(), - processes: Vec::new(), - preview_supported: preview_supported(provider, port), - }); - push_unique(&mut service.addresses, address.to_string()); - if let Some(process) = process { - push_unique(&mut service.processes, process); - } + push_service(&mut services, provider, port, address.to_string(), process); } - services.into_values().collect() + sorted_services(services) } fn parse_ss_local_port(address: &str) -> Option { @@ -624,6 +655,108 @@ fn parse_ss_local_port(address: &str) -> Option { (port > 0).then_some(port) } +#[derive(Clone, Copy)] +enum ProcNetFamily { + Ipv4, + Ipv6, +} + +fn parse_proc_net_listening_services(output: &str, provider: &str) -> Vec { + let mut services = BTreeMap::::new(); + let mut family = None; + for line in output + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + { + if let Some(path) = line.strip_prefix("FABRO_PROC_NET_TCP ") { + family = if path.ends_with("/tcp6") { + Some(ProcNetFamily::Ipv6) + } else { + Some(ProcNetFamily::Ipv4) + }; + continue; + } + if line.starts_with("sl") { + continue; + } + let Some(family) = family else { + continue; + }; + let fields = line.split_whitespace().collect::>(); + let (Some(local_address), Some(state)) = (fields.get(1), fields.get(3)) else { + continue; + }; + if *state != "0A" { + continue; + } + let Some((address, port)) = parse_proc_net_local_address(local_address, family) else { + continue; + }; + push_service(&mut services, provider, port, address, None); + } + sorted_services(services) +} + +fn sorted_services(services: BTreeMap) -> Vec { + let mut services = services.into_values().collect::>(); + services.sort_by_key(|service| (!service.preview_supported, service.port)); + services +} + +fn parse_proc_net_local_address(value: &str, family: ProcNetFamily) -> Option<(String, u16)> { + let (address_hex, port_hex) = value.split_once(':')?; + let port = u16::from_str_radix(port_hex, 16).ok()?; + if port == 0 { + return None; + } + let address = match family { + ProcNetFamily::Ipv4 => format!("{}:{port}", parse_proc_net_ipv4(address_hex)?), + ProcNetFamily::Ipv6 => format!("[{}]:{port}", parse_proc_net_ipv6(address_hex)?), + }; + Some((address, port)) +} + +fn parse_proc_net_ipv4(value: &str) -> Option { + if value.len() != 8 { + return None; + } + let raw = u32::from_str_radix(value, 16).ok()?; + Some(Ipv4Addr::from(raw.to_le_bytes())) +} + +fn parse_proc_net_ipv6(value: &str) -> Option { + if value.len() != 32 { + return None; + } + let mut bytes = [0_u8; 16]; + for (chunk_index, chunk) in value.as_bytes().chunks_exact(8).enumerate() { + let chunk = std::str::from_utf8(chunk).ok()?; + let raw = u32::from_str_radix(chunk, 16).ok()?; + bytes[chunk_index * 4..chunk_index * 4 + 4].copy_from_slice(&raw.to_le_bytes()); + } + Some(Ipv6Addr::from(bytes)) +} + +fn push_service( + services: &mut BTreeMap, + provider: &str, + port: u16, + address: String, + process: Option, +) { + let service = services.entry(port).or_insert_with(|| SandboxService { + port, + addresses: Vec::new(), + processes: Vec::new(), + preview_supported: preview_supported(provider, port), + }); + push_unique(&mut service.addresses, address); + if let Some(process) = process { + push_unique(&mut service.processes, process); + } +} + fn preview_supported(provider: &str, port: u16) -> bool { provider == SandboxProvider::Daytona.to_string() && (3000..=9999).contains(&port) } @@ -868,24 +1001,24 @@ LISTEN 0 4096 [::1]:2500 [::]:* users:(("debug",pid=168,fd=7)) ); assert_eq!(services.len(), 4); - assert_eq!(services[0].port, 2500); - assert_eq!(services[0].addresses, vec!["[::1]:2500"]); + assert_eq!(services[0].port, 3000); + assert_eq!(services[0].addresses, vec!["127.0.0.1:3000"]); assert_eq!(services[0].processes, vec![ - r#"users:(("debug",pid=168,fd=7))"# - ]); - assert!(!services[0].preview_supported); - assert_eq!(services[1].port, 3000); - assert_eq!(services[1].addresses, vec!["127.0.0.1:3000"]); - assert_eq!(services[1].processes, vec![ r#"users:(("node",pid=42,fd=23))"# ]); + assert!(services[0].preview_supported); + assert_eq!(services[1].port, 5173); + assert_eq!(services[1].addresses, vec!["0.0.0.0:5173"]); assert!(services[1].preview_supported); - assert_eq!(services[2].port, 5173); - assert_eq!(services[2].addresses, vec!["0.0.0.0:5173"]); + assert_eq!(services[2].port, 8080); + assert_eq!(services[2].addresses, vec!["[::]:8080"]); assert!(services[2].preview_supported); - assert_eq!(services[3].port, 8080); - assert_eq!(services[3].addresses, vec!["[::]:8080"]); - assert!(services[3].preview_supported); + assert_eq!(services[3].port, 2500); + assert_eq!(services[3].addresses, vec!["[::1]:2500"]); + assert_eq!(services[3].processes, vec![ + r#"users:(("debug",pid=168,fd=7))"# + ]); + assert!(!services[3].preview_supported); } #[test] @@ -931,6 +1064,52 @@ LISTEN 0 4096 [::]:3000 [::]:* users:(("vite",pid=84,fd=19)) }]); } + #[test] + fn proc_net_parser_extracts_listening_tcp_services_without_processes() { + let discovery = parse_sandbox_services( + r#" +FABRO_PROC_NET_TCP /proc/net/tcp + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 0100007F:0BB8 00000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 11111 + 1: 00000000:1435 00000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 22222 + 2: 0100007F:2328 00000000:0000 01 00000000:00000000 00:00000000 00000000 501 0 33333 +FABRO_PROC_NET_TCP /proc/net/tcp6 + sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 00000000000000000000000000000000:1F90 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 44444 + 1: 00000000000000000000000001000000:09C4 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 55555 +"#, + "daytona", + ); + + assert_eq!(discovery.source, SandboxServiceDiscoverySource::Procfs); + assert_eq!(discovery.services, vec![ + SandboxService { + port: 3000, + addresses: vec!["127.0.0.1:3000".to_string()], + processes: vec![], + preview_supported: true, + }, + SandboxService { + port: 5173, + addresses: vec!["0.0.0.0:5173".to_string()], + processes: vec![], + preview_supported: true, + }, + SandboxService { + port: 8080, + addresses: vec!["[::]:8080".to_string()], + processes: vec![], + preview_supported: true, + }, + SandboxService { + port: 2500, + addresses: vec!["[::1]:2500".to_string()], + processes: vec![], + preview_supported: false, + }, + ]); + } + #[test] fn preview_support_is_daytona_only_for_documented_range() { assert!(!preview_supported("daytona", 2500)); @@ -963,7 +1142,7 @@ LISTEN 0 4096 [::]:3000 [::]:* users:(("vite",pid=84,fd=19)) result.stdout.clear(); assert_eq!( sandbox_service_command_failure_detail(&result), - "ss -H -ltnp failed" + "sandbox service discovery command failed" ); } diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs index 9e7e38c86..03271cb33 100644 --- a/lib/crates/fabro-types/src/lib.rs +++ b/lib/crates/fabro-types/src/lib.rs @@ -81,7 +81,10 @@ pub use run_summary::RunSummary; pub use run_title::{RunTitleError, infer_run_title, normalize_explicit_run_title}; pub use sandbox_details::{SandboxDetails, SandboxResources, SandboxState, SandboxTimestamps}; pub use sandbox_record::SandboxRecord; -pub use sandbox_services::{SandboxService, SandboxServiceListResponse}; +pub use sandbox_services::{ + SandboxService, SandboxServiceDiscoverySource, SandboxServiceListMeta, + SandboxServiceListResponse, +}; pub use secret::{SecretMetadata, SecretType}; pub use stage_completion::StageCompletion; pub use stage_handler::StageHandler; diff --git a/lib/crates/fabro-types/src/sandbox_services.rs b/lib/crates/fabro-types/src/sandbox_services.rs index 7212c174b..48f61be32 100644 --- a/lib/crates/fabro-types/src/sandbox_services.rs +++ b/lib/crates/fabro-types/src/sandbox_services.rs @@ -1,3 +1,15 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SandboxServiceDiscoverySource { + Ss, + Procfs, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SandboxServiceListMeta { + pub source: SandboxServiceDiscoverySource, +} + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SandboxService { pub port: u16, @@ -9,4 +21,5 @@ pub struct SandboxService { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SandboxServiceListResponse { pub data: Vec, + pub meta: SandboxServiceListMeta, } diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 5cf2ab09d..55d3fc6e5 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -290,6 +290,8 @@ models/sandbox-details.ts models/sandbox-file-entry.ts models/sandbox-file-list-response.ts models/sandbox-resources.ts +models/sandbox-service-discovery-source.ts +models/sandbox-service-list-meta.ts models/sandbox-service-list-response.ts models/sandbox-service.ts models/sandbox-state.ts diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 083763307..95f805b5e 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -268,6 +268,8 @@ export * from './sandbox-file-entry'; export * from './sandbox-file-list-response'; export * from './sandbox-resources'; export * from './sandbox-service'; +export * from './sandbox-service-discovery-source'; +export * from './sandbox-service-list-meta'; export * from './sandbox-service-list-response'; export * from './sandbox-state'; export * from './sandbox-timestamps'; diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts b/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts new file mode 100644 index 000000000..78024461f --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Tool or kernel interface used to discover sandbox services. + */ + +export const SandboxServiceDiscoverySource = { + SS: 'ss', + PROCFS: 'procfs' +} as const; + +export type SandboxServiceDiscoverySource = typeof SandboxServiceDiscoverySource[keyof typeof SandboxServiceDiscoverySource]; + + + diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts b/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts new file mode 100644 index 000000000..4d45b0489 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { SandboxServiceDiscoverySource } from './sandbox-service-discovery-source'; + +/** + * Metadata about sandbox service discovery. + */ +export interface SandboxServiceListMeta { + 'source': SandboxServiceDiscoverySource; +} + + + diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts b/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts index 15a6c634c..86071eba6 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts @@ -16,11 +16,15 @@ // May contain unused imports in some cases // @ts-ignore import type { SandboxService } from './sandbox-service'; +// May contain unused imports in some cases +// @ts-ignore +import type { SandboxServiceListMeta } from './sandbox-service-list-meta'; /** * Non-paginated list of listening TCP services in a run sandbox. */ export interface SandboxServiceListResponse { 'data': Array; + 'meta': SandboxServiceListMeta; } diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service.ts b/lib/packages/fabro-api-client/src/models/sandbox-service.ts index 0550d38ec..3b4c246ed 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-service.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-service.ts @@ -23,11 +23,11 @@ export interface SandboxService { */ 'port': number; /** - * Local bind addresses reported by `ss`. + * Local bind addresses discovered from `ss` or `/proc/net/tcp*`. */ 'addresses': Array; /** - * Visible process summaries reported by `ss`. + * Visible process summaries when available. Empty when the sandbox only supports `/proc/net/tcp*` discovery. */ 'processes': Array; /**
PortBindingsProcessActionPortBindingsProcess +
{service.port} + {service.port} {service.addresses.length > 0 ? ( service.addresses.join(", ") ) : ( - )} + {service.processes.length > 0 ? ( service.processes.join(", ") ) : ( - )} + {service.preview_supported ? ( - ) : ( - Unavailable - )} + ) : null}