diff --git a/crates/trycp_api/src/lib.rs b/crates/trycp_api/src/lib.rs index a50cef5a..a3265f9b 100644 --- a/crates/trycp_api/src/lib.rs +++ b/crates/trycp_api/src/lib.rs @@ -106,6 +106,12 @@ pub enum Request { #[serde(with = "serde_bytes")] message: Vec, }, + + /// Request the logs for a player's conductor and keystore. + DownloadLogs { + /// The conductor id. + id: String, + }, } /// Message response types. @@ -153,3 +159,22 @@ pub enum MessageToClient { response: std::result::Result, }, } + +/// Messages returned directly by the TryCp server, rather than relayed from Holochain +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "data")] +pub enum TryCpServerResponse { + /// See [DownloadLogsResponse]. + DownloadLogs(DownloadLogsResponse), +} + +/// The successful response type for a [Request::DownloadLogs] request. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct DownloadLogsResponse { + /// The lair keystore stderr log. + pub lair_stderr: Vec, + /// The holochain conductor stdout log. + pub conductor_stdout: Vec, + /// The holochain conductor stderr log. + pub conductor_stderr: Vec, +} diff --git a/crates/trycp_server/src/download_logs.rs b/crates/trycp_server/src/download_logs.rs new file mode 100644 index 00000000..ab1fe99c --- /dev/null +++ b/crates/trycp_server/src/download_logs.rs @@ -0,0 +1,55 @@ +use crate::{ + get_player_dir, player_config_exists, CONDUCTOR_STDERR_LOG_FILENAME, + CONDUCTOR_STDOUT_LOG_FILENAME, LAIR_STDERR_LOG_FILENAME, +}; +use snafu::{ResultExt, Snafu}; +use trycp_api::{DownloadLogsResponse, MessageResponse, TryCpServerResponse}; + +#[derive(Debug, Snafu)] +pub(crate) enum DownloadLogsError { + #[snafu(display("No player with this ID is configured {}", id))] + PlayerNotConfigured { id: String }, + #[snafu(display("Could not read lair stderr log for player with ID {}: {}", id, source))] + LairStdErr { id: String, source: std::io::Error }, + #[snafu(display( + "Could not read holochain stdout log for player with ID {}: {}", + id, + source + ))] + HolochainStdout { id: String, source: std::io::Error }, + #[snafu(display( + "Could not read holochain stderr log for player with ID {}: {}", + id, + source + ))] + HolochainStderr { id: String, source: std::io::Error }, + #[snafu(display("Could not serialize response: {}", source))] + SerializeResponse { source: rmp_serde::encode::Error }, +} + +pub(crate) fn download_logs(id: String) -> Result { + if !player_config_exists(&id) { + return Err(DownloadLogsError::PlayerNotConfigured { id }); + } + + let player_dir = get_player_dir(&id); + + let lair_stderr = player_dir.join(LAIR_STDERR_LOG_FILENAME); + let lair_stderr = std::fs::read(&lair_stderr).context(LairStdErr { id: id.clone() })?; + + let conductor_stdout = player_dir.join(CONDUCTOR_STDOUT_LOG_FILENAME); + let conductor_stdout = + std::fs::read(&conductor_stdout).context(HolochainStdout { id: id.clone() })?; + + let conductor_stderr = player_dir.join(CONDUCTOR_STDERR_LOG_FILENAME); + let conductor_stderr = std::fs::read(&conductor_stderr).context(HolochainStderr { id })?; + + Ok(MessageResponse::Bytes( + rmp_serde::to_vec_named(&TryCpServerResponse::DownloadLogs(DownloadLogsResponse { + lair_stderr, + conductor_stdout, + conductor_stderr, + })) + .context(SerializeResponse)?, + )) +} diff --git a/crates/trycp_server/src/main.rs b/crates/trycp_server/src/main.rs index a660df69..8660f645 100644 --- a/crates/trycp_server/src/main.rs +++ b/crates/trycp_server/src/main.rs @@ -5,6 +5,7 @@ mod admin_call; mod app_interface; mod configure_player; mod download_dna; +mod download_logs; mod reset; mod save_dna; mod shutdown; @@ -252,6 +253,14 @@ async fn ws_message( Err(e) => serialize_resp(request_id, Err::<(), _>(e.to_string())), } } + Request::DownloadLogs { id } => spawn_blocking(move || { + serialize_resp( + request_id, + download_logs::download_logs(id).map_err(|e| e.to_string()), + ) + }) + .await + .unwrap(), }; Ok(Some(Message::Binary(response))) diff --git a/crates/trycp_server/src/startup.rs b/crates/trycp_server/src/startup.rs index c1b38a95..6aa96793 100644 --- a/crates/trycp_server/src/startup.rs +++ b/crates/trycp_server/src/startup.rs @@ -99,6 +99,10 @@ pub fn startup(id: String, log_level: Option) -> Result<(), Error> { .arg("--piped") .arg("-c") .arg(CONDUCTOR_CONFIG_FILENAME) + // Disable ANSI color codes in Holochain output, which should be set any time the output + // is being written to a file. + // See https://docs.rs/tracing-subscriber/0.3.18/tracing_subscriber/fmt/struct.Layer.html#method.with_ansi + .env("NO_COLOR", "1") .env("RUST_BACKTRACE", "full") .env("RUST_LOG", rust_log) .stdin(Stdio::piped()) diff --git a/ts/src/trycp/conductor/conductor.ts b/ts/src/trycp/conductor/conductor.ts index 07d04fb3..2ebd0ae2 100644 --- a/ts/src/trycp/conductor/conductor.ts +++ b/ts/src/trycp/conductor/conductor.ts @@ -53,6 +53,7 @@ import { makeLogger } from "../../logger.js"; import { AgentsAppsOptions, AppOptions, IConductor } from "../../types.js"; import { AdminApiResponseAppAuthenticationTokenIssued, + DownloadLogsResponse, TryCpClient, TryCpConductorLogLevel, } from "../index.js"; @@ -312,6 +313,18 @@ export class TryCpConductor implements IConductor { return response; } + async downloadLogs() { + const response = await this.tryCpClient.call({ + type: "download_logs", + id: this.id, + }); + assert(response !== TRYCP_SUCCESS_RESPONSE); + assert(typeof response === "object"); + assert("type" in response); + assert(response.type === "download_logs"); + return response.data; + } + /** * Attach a signal handler. * diff --git a/ts/src/trycp/types.ts b/ts/src/trycp/types.ts index b5d3c8e6..3ecf46d9 100644 --- a/ts/src/trycp/types.ts +++ b/ts/src/trycp/types.ts @@ -90,7 +90,8 @@ export type TryCpRequest = | RequestDisconnectAppInterface | RequestCallAppInterface | RequestCallAppInterfaceEncoded - | RequestCallAdminInterface; + | RequestCallAdminInterface + | RequestDownloadLogs; /** * Request to download a DNA from a URL. @@ -249,8 +250,22 @@ export type TryCpResponseErrorValue = string | Error; export type TryCpApiResponse = | AdminApiResponse | AppApiResponse + | TryCpControlResponse | ApiErrorResponse; +export type TryCpControlResponse = DownloadLogsResponseType; + +export interface DownloadLogsResponseType { + type: "download_logs"; + data: DownloadLogsResponse; +} + +export interface DownloadLogsResponse { + lair_stderr: Uint8Array; + conductor_stdout: Uint8Array; + conductor_stderr: Uint8Array; +} + /** * Error response from the Admin or App API. * @@ -479,6 +494,11 @@ export interface RequestCallAdminInterface { message: RequestAdminInterfaceMessage; } +export interface RequestDownloadLogs { + type: "download_logs"; + id: ConductorId; +} + /** * The types of all possible calls to the Admin API. * diff --git a/ts/test/trycp/client.ts b/ts/test/trycp/client.ts index 8182f530..78b46e9e 100644 --- a/ts/test/trycp/client.ts +++ b/ts/test/trycp/client.ts @@ -314,3 +314,20 @@ test("TryCP Server - App API - get app info", async (t) => { await conductor.disconnectClient(); await localTryCpServer.stop(); }); + +test("TryCP Client - download logs", async (t) => { + const localTryCpServer = await TryCpServer.start(); + const tryCpClient = await createTryCpClient(); + const conductor = await createTryCpConductor(tryCpClient); + const logs = await conductor.downloadLogs(); + + t.true(logs.lair_stderr.length === 0, "lair stderr logs are empty"); + t.true( + logs.conductor_stdout.length > 0, + "conductor stdout logs are not empty" + ); + t.true(logs.conductor_stderr.length === 0, "conductor stderr logs are empty"); + + await conductor.shutDown(); + await localTryCpServer.stop(); +});