diff --git a/backend/embassy-init.service b/backend/embassy-init.service index 05d11b575..104f16908 100644 --- a/backend/embassy-init.service +++ b/backend/embassy-init.service @@ -6,7 +6,7 @@ Wants=avahi-daemon.service nginx.service tor.service [Service] Type=oneshot -Environment=RUST_LOG=embassy_init=debug,embassy=debug +Environment=RUST_LOG=embassy_init=debug,embassy=debug,js_engine=debug ExecStart=/usr/local/bin/embassy-init RemainAfterExit=true diff --git a/backend/embassyd.service b/backend/embassyd.service index 1ed510586..f5ca4c4e8 100644 --- a/backend/embassyd.service +++ b/backend/embassyd.service @@ -5,7 +5,7 @@ Requires=embassy-init.service [Service] Type=simple -Environment=RUST_LOG=embassyd=debug,embassy=debug +Environment=RUST_LOG=embassyd=debug,embassy=debug,js_engine=debug ExecStart=/usr/local/bin/embassyd Restart=always RestartSec=3 diff --git a/backend/src/bin/embassy-sdk.rs b/backend/src/bin/embassy-sdk.rs index 0e962f8f1..9a77c9988 100644 --- a/backend/src/bin/embassy-sdk.rs +++ b/backend/src/bin/embassy-sdk.rs @@ -20,7 +20,7 @@ fn inner_main() -> Result<(), Error> { ), context: matches => { if let Err(_) = std::env::var("RUST_LOG") { - std::env::set_var("RUST_LOG", "embassy=warn"); + std::env::set_var("RUST_LOG", "embassy=warn,js_engine=warn"); } EmbassyLogger::init(); SdkContext::init(matches)? diff --git a/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js b/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js index d25ca5f77..8621c3abf 100644 --- a/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js +++ b/backend/test/js_action_execute/package-data/scripts/test-package/0.3.0.3/embassy.js @@ -89,6 +89,42 @@ export async function getConfig(effects) { effects.warn("warn"); effects.error("error"); effects.info("info"); + + { + const metadata = await effects.metadata({ + path: "./test.log", + volumeId: "main", + }) + + if (typeof metadata.fileType !== 'string') { + throw new TypeError("File type is not a string") + } + if (typeof metadata.isDir !== 'boolean' ) { + throw new TypeError("isDir is not a boolean") + } + if (typeof metadata.isFile !== 'boolean' ) { + throw new TypeError("isFile is not a boolean") + } + if (typeof metadata.isSymlink !== 'boolean' ) { + throw new TypeError("isSymlink is not a boolean") + } + if (typeof metadata.len !== 'number' ) { + throw new TypeError("len is not a number") + } + if (!(metadata.modified instanceof Date )) { + throw new TypeError("modified is not a Date") + } + if (!(metadata.accessed instanceof Date )) { + throw new TypeError("accessed is not a Date") + } + if (!(metadata.created instanceof Date )) { + throw new TypeError("created is not a Date") + } + if (typeof metadata.readonly !== 'boolean' ) { + throw new TypeError("readonly is not a boolean") + } + effects.error(JSON.stringify(metadata)) + } return { result: { spec: { diff --git a/libs/artifacts/types.d.ts b/libs/artifacts/types.d.ts index 559e53396..a1846e631 100644 --- a/libs/artifacts/types.d.ts +++ b/libs/artifacts/types.d.ts @@ -26,6 +26,7 @@ export namespace ExpectedExports { ) => Promise>; } + /** Used to reach out from the pure js runtime */ export type Effects = { /** Usable when not sandboxed */ @@ -33,6 +34,7 @@ export type Effects = { input: { path: string; volumeId: string; toWrite: string }, ): Promise; readFile(input: { volumeId: string; path: string }): Promise; + metadata(input: { volumeId: string; path: string }): Promise; /** Create a directory. Usable when not sandboxed */ createDir(input: { volumeId: string; path: string }): Promise; /** Remove a directory. Usable when not sandboxed */ @@ -60,7 +62,20 @@ export type Effects = { /** Sandbox mode lets us read but not write */ is_sandboxed(): boolean; + }; +export type Metadata = { + + fileType: string, + isDir: boolean, + isFile: boolean, + isSymlink: boolean, + len: number, + modified?: Date, + accessed?: Date, + created?: Date, + readonly: boolean, +} export type MigrationRes = { configured: boolean; diff --git a/libs/js_engine/src/artifacts/loadModule.js b/libs/js_engine/src/artifacts/loadModule.js index fd868f577..a0324b9c3 100644 --- a/libs/js_engine/src/artifacts/loadModule.js +++ b/libs/js_engine/src/artifacts/loadModule.js @@ -1,7 +1,5 @@ -//@ts-check -// @ts-ignore + import Deno from "/deno_global.js"; -// @ts-ignore import * as mainModule from "/embassy.js"; /** * This is using the simplified json pointer spec, using no escapes and arrays @@ -20,45 +18,42 @@ function jsonPointerValue(obj, pointer) { return obj; } -// @ts-ignore +function maybeDate(value) { + if (!value) return value; + return new Date(value) +} const writeFile = ({ path, volumeId, toWrite }) => Deno.core.opAsync("write_file", volumeId, path, toWrite); -// @ts-ignore const readFile = ({ volumeId, path }) => Deno.core.opAsync("read_file", volumeId, path); -// @ts-ignore +const metadata = async ({ volumeId, path }) => { + const data = await Deno.core.opAsync("metadata", volumeId, path) + return { + ...data, + modified: maybeDate(data.modified), + created: maybeDate(data.created), + accessed: maybeDate(data.accessed), + } +}; const removeFile = ({ volumeId, path }) => Deno.core.opAsync("remove_file", volumeId, path); -// @ts-ignore const isSandboxed = () => Deno.core.opSync("is_sandboxed"); -// @ts-ignore const writeJsonFile = ({ volumeId, path, toWrite }) => writeFile({ volumeId, path, toWrite: JSON.stringify(toWrite), }); -// @ts-ignore const readJsonFile = async ({ volumeId, path }) => JSON.parse(await readFile({ volumeId, path })); -// @ts-ignore const createDir = ({ volumeId, path }) => Deno.core.opAsync("create_dir", volumeId, path); -// @ts-ignore const removeDir = ({ volumeId, path }) => Deno.core.opAsync("remove_dir", volumeId, path); -// @ts-ignore const trace = (x) => Deno.core.opSync("log_trace", x); -// @ts-ignore const warn = (x) => Deno.core.opSync("log_warn", x); -// @ts-ignore const error = (x) => Deno.core.opSync("log_error", x); -// @ts-ignore const debug = (x) => Deno.core.opSync("log_debug", x); -// @ts-ignore const info = (x) => Deno.core.opSync("log_info", x); -// @ts-ignore const currentFunction = Deno.core.opSync("current_function"); -//@ts-ignore const input = Deno.core.opSync("get_input"); -// @ts-ignore const setState = (x) => Deno.core.opSync("set_value", x); const effects = { writeFile, @@ -74,6 +69,7 @@ const effects = { removeFile, createDir, removeDir, + metadata }; const runFunction = jsonPointerValue(mainModule, currentFunction); diff --git a/libs/js_engine/src/lib.rs b/libs/js_engine/src/lib.rs index 2213da433..0545b90e0 100644 --- a/libs/js_engine/src/lib.rs +++ b/libs/js_engine/src/lib.rs @@ -15,6 +15,7 @@ use helpers::NonDetachingJoinHandle; use models::{PackageId, ProcedureName, Version, VolumeId}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::time::SystemTime; use std::{path::Path, sync::Arc}; use std::{path::PathBuf, pin::Pin}; use tokio::io::AsyncReadExt; @@ -60,6 +61,20 @@ impl JsError { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MetadataJs { + file_type: String, + is_dir: bool, + is_file: bool, + is_symlink: bool, + len: u64, + modified: Option, + accessed: Option, + created: Option, + readonly: bool, +} + #[cfg(target_arch = "x86_64")] const SNAPSHOT_BYTES: &[u8] = include_bytes!("./artifacts/JS_SNAPSHOT.bin"); @@ -240,6 +255,7 @@ impl JsExecutionEnvironment { fn declarations() -> Vec { vec![ fns::read_file::decl(), + fns::metadata::decl(), fns::write_file::decl(), fns::remove_file::decl(), fns::create_dir::decl(), @@ -332,6 +348,8 @@ mod fns { use models::VolumeId; + use crate::{system_time_as_unix_ms, MetadataJs}; + use super::{AnswerState, JsContext}; #[op] @@ -359,6 +377,54 @@ mod fns { Ok(answer) } #[op] + async fn metadata( + state: Rc>, + volume_id: VolumeId, + path_in: PathBuf, + ) -> Result { + let state = state.borrow(); + let ctx: &JsContext = state.borrow(); + let volume_path = ctx + .volumes + .path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id) + .ok_or_else(|| anyhow!("There is no {} in volumes", volume_id))?; + //get_path_for in volume.rs + let new_file = volume_path.join(path_in); + if !is_subset(&volume_path, &new_file).await? { + bail!( + "Path '{}' has broken away from parent '{}'", + new_file.to_string_lossy(), + volume_path.to_string_lossy(), + ); + } + let answer = tokio::fs::metadata(new_file).await?; + let metadata_js = MetadataJs { + file_type: format!("{:?}", answer.file_type()), + is_dir: answer.is_dir(), + is_file: answer.is_file(), + is_symlink: answer.is_symlink(), + len: answer.len(), + modified: answer + .modified() + .ok() + .as_ref() + .and_then(system_time_as_unix_ms), + accessed: answer + .accessed() + .ok() + .as_ref() + .and_then(system_time_as_unix_ms), + created: answer + .created() + .ok() + .as_ref() + .and_then(system_time_as_unix_ms), + readonly: answer.permissions().readonly(), + }; + + Ok(metadata_js) + } + #[op] async fn write_file( state: Rc>, volume_id: VolumeId, @@ -564,3 +630,12 @@ mod fns { Ok(child.starts_with(parent)) } } + +fn system_time_as_unix_ms(system_time: &SystemTime) -> Option { + system_time + .duration_since(SystemTime::UNIX_EPOCH) + .ok()? + .as_millis() + .try_into() + .ok() +}