mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
Chore/version 0 3 1 0 (#1475)
* feat: move over to workspaces * chore: Move to libs * chore:fix(build): Compat * chore: fixing pr
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::eyre;
|
||||
@@ -11,7 +9,7 @@ use tracing::instrument;
|
||||
|
||||
use crate::config::{Config, ConfigSpec};
|
||||
use crate::context::RpcContext;
|
||||
use crate::id::{Id, ImageId, InvalidId};
|
||||
use crate::id::{ ImageId};
|
||||
use crate::procedure::{PackageProcedure, ProcedureName};
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::{display_serializable, parse_stdin_deserializable, IoFormat};
|
||||
@@ -19,52 +17,7 @@ use crate::util::Version;
|
||||
use crate::volume::Volumes;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
pub struct ActionId<S: AsRef<str> = String>(Id<S>);
|
||||
impl FromStr for ActionId {
|
||||
type Err = InvalidId;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(ActionId(Id::try_from(s.to_owned())?))
|
||||
}
|
||||
}
|
||||
impl From<ActionId> for String {
|
||||
fn from(value: ActionId) -> Self {
|
||||
value.0.into()
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<ActionId<S>> for ActionId<S> {
|
||||
fn as_ref(&self) -> &ActionId<S> {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::fmt::Display for ActionId<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<str> for ActionId<S> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<Path> for ActionId<S> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de, S> Deserialize<'de> for ActionId<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
Id<S>: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
Ok(ActionId(Deserialize::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
|
||||
pub use models::ActionId;
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Actions(pub BTreeMap<ActionId, Action>);
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
fn main() {
|
||||
let mut runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
|
||||
will_snapshot: true,
|
||||
..Default::default()
|
||||
});
|
||||
let snapshot = runtime.snapshot();
|
||||
|
||||
let snapshot_slice: &[u8] = &*snapshot;
|
||||
println!("Snapshot size: {}", snapshot_slice.len());
|
||||
|
||||
std::fs::write("JS_SNAPSHOT.bin", snapshot_slice).unwrap();
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use models::InvalidId;
|
||||
use patch_db::Revision;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
|
||||
@@ -144,6 +145,7 @@ pub struct Error {
|
||||
pub kind: ErrorKind,
|
||||
pub revision: Option<Revision>,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}: {}", self.kind.as_str(), self.source)
|
||||
@@ -158,6 +160,11 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<InvalidId> for Error {
|
||||
fn from(err: InvalidId) -> Self {
|
||||
Error::new(err, crate::error::ErrorKind::InvalidPackageId)
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::new(e, ErrorKind::Filesystem)
|
||||
|
||||
@@ -1,142 +1,13 @@
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::fmt::Debug;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
use crate::util::Version;
|
||||
use crate::Error;
|
||||
|
||||
pub const SYSTEM_ID: Id<&'static str> = Id("x_system");
|
||||
pub use models::{Id, InvalidId, IdUnchecked, SYSTEM_ID};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Invalid ID")]
|
||||
pub struct InvalidId;
|
||||
impl From<InvalidId> for Error {
|
||||
fn from(err: InvalidId) -> Self {
|
||||
Error::new(err, crate::error::ErrorKind::InvalidPackageId)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct IdUnchecked<S: AsRef<str>>(pub S);
|
||||
impl<'de> Deserialize<'de> for IdUnchecked<Cow<'de, str>> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct Visitor;
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = IdUnchecked<Cow<'de, str>>;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "a valid ID")
|
||||
}
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(IdUnchecked(Cow::Owned(v.to_owned())))
|
||||
}
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(IdUnchecked(Cow::Owned(v)))
|
||||
}
|
||||
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(IdUnchecked(Cow::Borrowed(v)))
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_any(Visitor)
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for IdUnchecked<String> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(IdUnchecked(String::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for IdUnchecked<&'de str> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(IdUnchecked(<&'de str>::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Id<S: AsRef<str> = String>(S);
|
||||
impl<S: AsRef<str>> Id<S> {
|
||||
pub fn try_from(value: S) -> Result<Self, InvalidId> {
|
||||
if value
|
||||
.as_ref()
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c == '-')
|
||||
{
|
||||
Ok(Id(value))
|
||||
} else {
|
||||
Err(InvalidId)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> Id<&'a str> {
|
||||
pub fn owned(&self) -> Id {
|
||||
Id(self.0.to_owned())
|
||||
}
|
||||
}
|
||||
impl From<Id> for String {
|
||||
fn from(value: Id) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::ops::Deref for Id<S> {
|
||||
type Target = S;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::fmt::Display for Id<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0.as_ref())
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<str> for Id<S> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> Borrow<str> for Id<S> {
|
||||
fn borrow(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de, S> Deserialize<'de> for Id<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
IdUnchecked<S>: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let unchecked: IdUnchecked<S> = Deserialize::deserialize(deserializer)?;
|
||||
Id::try_from(unchecked.0).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> Serialize for Id<S> {
|
||||
fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
|
||||
where
|
||||
Ser: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
pub struct ImageId<S: AsRef<str> = String>(Id<S>);
|
||||
|
||||
@@ -59,7 +59,6 @@ pub const PKG_ARCHIVE_DIR: &str = "package-data/archive";
|
||||
pub const PKG_PUBLIC_DIR: &str = "package-data/public";
|
||||
pub const PKG_DOCKER_DIR: &str = "package-data/docker";
|
||||
pub const PKG_WASM_DIR: &str = "package-data/wasm";
|
||||
pub const PKG_SCRIPT_DIR: &str = "package-data/scripts";
|
||||
|
||||
#[command(display(display_serializable))]
|
||||
pub async fn list(#[context] ctx: RpcContext) -> Result<Vec<(PackageId, Version)>, Error> {
|
||||
|
||||
@@ -393,6 +393,7 @@ impl Manager {
|
||||
let _ = self.shared.on_stop.send(OnStop::Exit);
|
||||
let action = match &self.shared.manifest.main {
|
||||
PackageProcedure::Docker(a) => a,
|
||||
#[cfg(feature = "js_engine")]
|
||||
PackageProcedure::Script(_) => return Ok(()),
|
||||
};
|
||||
match self
|
||||
@@ -546,6 +547,7 @@ async fn stop(shared: &ManagerSharedState) -> Result<(), Error> {
|
||||
}
|
||||
let action = match &shared.manifest.main {
|
||||
PackageProcedure::Docker(a) => a,
|
||||
#[cfg(feature = "js_engine")]
|
||||
PackageProcedure::Script(_) => return Ok(()),
|
||||
};
|
||||
match shared
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::TryStreamExt;
|
||||
@@ -16,6 +15,8 @@ use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::Port;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub use models::InterfaceId;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Interfaces(pub BTreeMap<InterfaceId, Interface>); // TODO
|
||||
@@ -113,46 +114,6 @@ impl Interfaces {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
pub struct InterfaceId<S: AsRef<str> = String>(Id<S>);
|
||||
impl<S: AsRef<str>> From<Id<S>> for InterfaceId<S> {
|
||||
fn from(id: Id<S>) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::fmt::Display for InterfaceId<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::ops::Deref for InterfaceId<S> {
|
||||
type Target = S;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<str> for InterfaceId<S> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de, S> Deserialize<'de> for InterfaceId<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
Id<S>: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(InterfaceId(Deserialize::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<Path> for InterfaceId<S> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use std::{path::{PathBuf, Path}, time::Duration};
|
||||
|
||||
use models::VolumeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -7,22 +8,24 @@ use crate::{
|
||||
context::RpcContext, s9pk::manifest::PackageId, util::Version, volume::Volumes, Error,
|
||||
};
|
||||
|
||||
use self::js_runtime::JsExecutionEnvironment;
|
||||
use js_engine::{JsExecutionEnvironment, PathForVolumeId};
|
||||
|
||||
use super::ProcedureName;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct JsCode(String);
|
||||
pub use js_engine::{JsError};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum JsError {
|
||||
Unknown = 1,
|
||||
Javascript = 2,
|
||||
Engine = 3,
|
||||
BoundryLayerSerDe = 4,
|
||||
Tokio = 5,
|
||||
FileSystem = 6,
|
||||
Timeout = 143,
|
||||
|
||||
impl PathForVolumeId for Volumes {
|
||||
fn path_for(&self, data_dir: &Path, package_id: &PackageId, version: &Version, volume_id: &VolumeId) -> Option<PathBuf> {
|
||||
|
||||
let volume = self.get(volume_id)?;
|
||||
Some(volume.path_for(data_dir, package_id, version, volume_id))
|
||||
}
|
||||
|
||||
fn readonly(&self,volume_id: &VolumeId) -> bool {
|
||||
self.get(volume_id).map(|x| x.readonly()).unwrap_or(false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
@@ -50,7 +53,7 @@ impl JsProcedure {
|
||||
directory,
|
||||
pkg_id,
|
||||
pkg_version,
|
||||
volumes.clone(),
|
||||
Box::new(volumes.clone()),
|
||||
)
|
||||
.await?
|
||||
.run_action(name, input);
|
||||
@@ -82,7 +85,7 @@ impl JsProcedure {
|
||||
&ctx.datadir,
|
||||
pkg_id,
|
||||
pkg_version,
|
||||
volumes.clone(),
|
||||
Box::new(volumes.clone()),
|
||||
)
|
||||
.await?
|
||||
.read_only_effects()
|
||||
@@ -100,564 +103,6 @@ impl JsProcedure {
|
||||
}
|
||||
}
|
||||
|
||||
mod js_runtime {
|
||||
use deno_core::anyhow::{anyhow, bail};
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::resolve_import;
|
||||
use deno_core::JsRuntime;
|
||||
use deno_core::ModuleLoader;
|
||||
use deno_core::ModuleSource;
|
||||
use deno_core::ModuleSourceFuture;
|
||||
use deno_core::ModuleSpecifier;
|
||||
use deno_core::ModuleType;
|
||||
use deno_core::RuntimeOptions;
|
||||
use deno_core::Snapshot;
|
||||
use deno_core::{Extension, OpDecl};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use std::{path::PathBuf, pin::Pin};
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::Version;
|
||||
use crate::volume::script_dir;
|
||||
use crate::volume::Volumes;
|
||||
|
||||
use super::super::ProcedureName;
|
||||
use super::{JsCode, JsError};
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
const SNAPSHOT_BYTES: &[u8] = include_bytes!("./js_scripts/JS_SNAPSHOT.bin");
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
const SNAPSHOT_BYTES: &[u8] = include_bytes!("./js_scripts/ARM_JS_SNAPSHOT.bin");
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
struct JsContext {
|
||||
sandboxed: bool,
|
||||
datadir: PathBuf,
|
||||
run_function: String,
|
||||
version: Version,
|
||||
package_id: PackageId,
|
||||
volumes: Arc<Volumes>,
|
||||
input: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct AnswerState(std::sync::Arc<deno_core::parking_lot::Mutex<Value>>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ModsLoader {
|
||||
code: JsCode,
|
||||
}
|
||||
|
||||
impl ModuleLoader for ModsLoader {
|
||||
fn resolve(
|
||||
&self,
|
||||
specifier: &str,
|
||||
referrer: &str,
|
||||
_is_main: bool,
|
||||
) -> Result<ModuleSpecifier, AnyError> {
|
||||
if referrer.contains("embassy") {
|
||||
bail!("Embassy.js cannot import anything else");
|
||||
}
|
||||
let s = resolve_import(specifier, referrer).unwrap();
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn load(
|
||||
&self,
|
||||
module_specifier: &ModuleSpecifier,
|
||||
maybe_referrer: Option<ModuleSpecifier>,
|
||||
is_dyn_import: bool,
|
||||
) -> Pin<Box<ModuleSourceFuture>> {
|
||||
let module_specifier = module_specifier.as_str().to_owned();
|
||||
let module = match &*module_specifier {
|
||||
"file:///deno_global.js" => Ok(ModuleSource {
|
||||
module_url_specified: "file:///deno_global.js".to_string(),
|
||||
module_url_found: "file:///deno_global.js".to_string(),
|
||||
code: "const old_deno = Deno; Deno = null; export default old_deno"
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
.into_boxed_slice(),
|
||||
module_type: ModuleType::JavaScript,
|
||||
}),
|
||||
"file:///loadModule.js" => Ok(ModuleSource {
|
||||
module_url_specified: "file:///loadModule.js".to_string(),
|
||||
module_url_found: "file:///loadModule.js".to_string(),
|
||||
code: include_str!("./js_scripts/loadModule.js")
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
.into_boxed_slice(),
|
||||
module_type: ModuleType::JavaScript,
|
||||
}),
|
||||
"file:///embassy.js" => Ok(ModuleSource {
|
||||
module_url_specified: "file:///embassy.js".to_string(),
|
||||
module_url_found: "file:///embassy.js".to_string(),
|
||||
code: self.code.0.as_bytes().to_vec().into_boxed_slice(),
|
||||
module_type: ModuleType::JavaScript,
|
||||
}),
|
||||
x => Err(anyhow!("Not allowed to import: {}", x)),
|
||||
};
|
||||
Box::pin(async move {
|
||||
if is_dyn_import {
|
||||
bail!("Will not import dynamic");
|
||||
}
|
||||
match &maybe_referrer {
|
||||
Some(x) if x.as_str() == "file:///embassy.js" => {
|
||||
bail!("Embassy is not allowed to import")
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
module
|
||||
})
|
||||
}
|
||||
}
|
||||
pub struct JsExecutionEnvironment {
|
||||
sandboxed: bool,
|
||||
base_directory: PathBuf,
|
||||
module_loader: ModsLoader,
|
||||
package_id: PackageId,
|
||||
version: Version,
|
||||
volumes: Arc<Volumes>,
|
||||
}
|
||||
|
||||
impl JsExecutionEnvironment {
|
||||
pub async fn load_from_package(
|
||||
data_directory: impl AsRef<std::path::Path>,
|
||||
package_id: &crate::s9pk::manifest::PackageId,
|
||||
version: &crate::util::Version,
|
||||
volumes: Volumes,
|
||||
) -> Result<Self, (JsError, String)> {
|
||||
let data_dir = data_directory.as_ref();
|
||||
let base_directory = data_dir;
|
||||
let js_code = JsCode({
|
||||
let file_path = script_dir(data_dir, package_id, version).join("embassy.js");
|
||||
let mut file = match tokio::fs::File::open(file_path.clone()).await {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
tracing::debug!("path: {:?}", file_path);
|
||||
tracing::debug!("{:?}", e);
|
||||
return Err((
|
||||
JsError::FileSystem,
|
||||
format!("The file opening '{:?}' created error: {}", file_path, e),
|
||||
));
|
||||
}
|
||||
};
|
||||
let mut buffer = Default::default();
|
||||
if let Err(err) = file.read_to_string(&mut buffer).await {
|
||||
tracing::debug!("{:?}", err);
|
||||
return Err((
|
||||
JsError::FileSystem,
|
||||
format!("The file reading created error: {}", err),
|
||||
));
|
||||
};
|
||||
buffer
|
||||
});
|
||||
Ok(Self {
|
||||
base_directory: base_directory.to_owned(),
|
||||
module_loader: ModsLoader { code: js_code },
|
||||
package_id: package_id.clone(),
|
||||
version: version.clone(),
|
||||
volumes: Arc::new(volumes),
|
||||
sandboxed: false,
|
||||
})
|
||||
}
|
||||
pub fn read_only_effects(mut self) -> Self {
|
||||
self.sandboxed = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn run_action<I: Serialize, O: for<'de> Deserialize<'de>>(
|
||||
self,
|
||||
procedure_name: ProcedureName,
|
||||
input: Option<I>,
|
||||
) -> Result<O, (JsError, String)> {
|
||||
let input = match serde_json::to_value(input) {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
tracing::error!("{}", err);
|
||||
tracing::debug!("{:?}", err);
|
||||
return Err((
|
||||
JsError::BoundryLayerSerDe,
|
||||
"Couldn't convert input".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let safer_handle: crate::util::NonDetachingJoinHandle<_> =
|
||||
tokio::task::spawn_blocking(move || self.execute(procedure_name, input)).into();
|
||||
let output = safer_handle
|
||||
.await
|
||||
.map_err(|err| (JsError::Tokio, format!("Tokio gave us the error: {}", err)))??;
|
||||
match serde_json::from_value(output.clone()) {
|
||||
Ok(x) => Ok(x),
|
||||
Err(err) => {
|
||||
tracing::error!("{}", err);
|
||||
tracing::debug!("{:?}", err);
|
||||
return Err((
|
||||
JsError::BoundryLayerSerDe,
|
||||
format!(
|
||||
"Couldn't convert output = {:#?} to the correct type",
|
||||
serde_json::to_string_pretty(&output).unwrap_or_default()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn declarations() -> Vec<OpDecl> {
|
||||
vec![
|
||||
fns::read_file::decl(),
|
||||
fns::write_file::decl(),
|
||||
fns::remove_file::decl(),
|
||||
fns::create_dir::decl(),
|
||||
fns::remove_dir::decl(),
|
||||
fns::current_function::decl(),
|
||||
fns::log_trace::decl(),
|
||||
fns::log_warn::decl(),
|
||||
fns::log_error::decl(),
|
||||
fns::log_debug::decl(),
|
||||
fns::log_info::decl(),
|
||||
fns::get_context::decl(),
|
||||
fns::get_input::decl(),
|
||||
fns::set_value::decl(),
|
||||
fns::is_sandboxed::decl(),
|
||||
]
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
procedure_name: ProcedureName,
|
||||
input: Value,
|
||||
) -> Result<Value, (JsError, String)> {
|
||||
let base_directory = self.base_directory.clone();
|
||||
let answer_state = AnswerState::default();
|
||||
let ext_answer_state = answer_state.clone();
|
||||
let js_ctx = JsContext {
|
||||
datadir: base_directory,
|
||||
run_function: procedure_name.js_function_name(),
|
||||
package_id: self.package_id.clone(),
|
||||
volumes: self.volumes.clone(),
|
||||
version: self.version.clone(),
|
||||
sandboxed: self.sandboxed,
|
||||
input,
|
||||
};
|
||||
let ext = Extension::builder()
|
||||
.ops(Self::declarations())
|
||||
.state(move |state| {
|
||||
state.put(ext_answer_state.clone());
|
||||
state.put(js_ctx.clone());
|
||||
Ok(())
|
||||
})
|
||||
.build();
|
||||
|
||||
let loader = std::rc::Rc::new(self.module_loader.clone());
|
||||
let runtime_options = RuntimeOptions {
|
||||
module_loader: Some(loader),
|
||||
extensions: vec![ext],
|
||||
startup_snapshot: Some(Snapshot::Static(SNAPSHOT_BYTES)),
|
||||
..Default::default()
|
||||
};
|
||||
let mut runtime = JsRuntime::new(runtime_options);
|
||||
|
||||
let future = async move {
|
||||
let mod_id = runtime
|
||||
.load_main_module(&"file:///loadModule.js".parse().unwrap(), None)
|
||||
.await?;
|
||||
let evaluated = runtime.mod_evaluate(mod_id);
|
||||
let res = runtime.run_event_loop(false).await;
|
||||
res?;
|
||||
evaluated.await??;
|
||||
Ok::<_, AnyError>(())
|
||||
};
|
||||
|
||||
tokio::runtime::Handle::current()
|
||||
.block_on(future)
|
||||
.map_err(|e| {
|
||||
tracing::debug!("{:?}", e);
|
||||
(JsError::Javascript, format!("{}", e))
|
||||
})?;
|
||||
|
||||
let answer = answer_state.0.lock().clone();
|
||||
Ok(answer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: Make sure that we have the assumption that all these methods are callable at any time, and all call restrictions should be in rust
|
||||
mod fns {
|
||||
use deno_core::{
|
||||
anyhow::{anyhow, bail},
|
||||
error::AnyError,
|
||||
*,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::volume::VolumeId;
|
||||
|
||||
use super::{AnswerState, JsContext};
|
||||
|
||||
#[op]
|
||||
async fn read_file(
|
||||
ctx: JsContext,
|
||||
volume_id: VolumeId,
|
||||
path_in: PathBuf,
|
||||
) -> Result<String, AnyError> {
|
||||
let volume = match ctx.volumes.get(&volume_id) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
bail!("There is no {} in volumes", volume_id);
|
||||
}
|
||||
};
|
||||
let volume_path =
|
||||
volume.path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &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::read_to_string(new_file).await?;
|
||||
Ok(answer)
|
||||
}
|
||||
#[op]
|
||||
async fn write_file(
|
||||
ctx: JsContext,
|
||||
volume_id: VolumeId,
|
||||
path_in: PathBuf,
|
||||
write: String,
|
||||
) -> Result<(), AnyError> {
|
||||
if ctx.sandboxed {
|
||||
bail!("Cannot write in sandbox mode");
|
||||
}
|
||||
let volume = match ctx.volumes.get(&volume_id) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
bail!("There is no {} in volumes", volume_id);
|
||||
}
|
||||
};
|
||||
if volume.readonly() {
|
||||
bail!("Volume {} is readonly", volume_id);
|
||||
}
|
||||
let volume_path =
|
||||
volume.path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id);
|
||||
|
||||
let new_file = volume_path.join(path_in);
|
||||
let parent_new_file = new_file
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Expecting that file is not root"))?;
|
||||
// With the volume check
|
||||
if !is_subset(&volume_path, &parent_new_file).await? {
|
||||
bail!(
|
||||
"Path '{}' has broken away from parent '{}'",
|
||||
new_file.to_string_lossy(),
|
||||
volume_path.to_string_lossy(),
|
||||
);
|
||||
}
|
||||
tokio::fs::write(new_file, write).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[op]
|
||||
async fn remove_file(
|
||||
ctx: JsContext,
|
||||
volume_id: VolumeId,
|
||||
path_in: PathBuf,
|
||||
) -> Result<(), AnyError> {
|
||||
if ctx.sandboxed {
|
||||
bail!("Cannot write in sandbox mode");
|
||||
}
|
||||
let volume = match ctx.volumes.get(&volume_id) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
bail!("There is no {} in volumes", volume_id);
|
||||
}
|
||||
};
|
||||
if volume.readonly() {
|
||||
bail!("Volume {} is readonly", volume_id);
|
||||
}
|
||||
let volume_path =
|
||||
volume.path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id);
|
||||
let new_file = volume_path.join(path_in);
|
||||
// With the volume check
|
||||
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(),
|
||||
);
|
||||
}
|
||||
tokio::fs::remove_file(new_file).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[op]
|
||||
async fn remove_dir(
|
||||
ctx: JsContext,
|
||||
volume_id: VolumeId,
|
||||
path_in: PathBuf,
|
||||
) -> Result<(), AnyError> {
|
||||
if ctx.sandboxed {
|
||||
bail!("Cannot write in sandbox mode");
|
||||
}
|
||||
let volume = match ctx.volumes.get(&volume_id) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
bail!("There is no {} in volumes", volume_id);
|
||||
}
|
||||
};
|
||||
if volume.readonly() {
|
||||
bail!("Volume {} is readonly", volume_id);
|
||||
}
|
||||
let volume_path =
|
||||
volume.path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id);
|
||||
let new_file = volume_path.join(path_in);
|
||||
// With the volume check
|
||||
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(),
|
||||
);
|
||||
}
|
||||
tokio::fs::remove_dir_all(new_file).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[op]
|
||||
async fn create_dir(
|
||||
ctx: JsContext,
|
||||
volume_id: VolumeId,
|
||||
path_in: PathBuf,
|
||||
) -> Result<(), AnyError> {
|
||||
if ctx.sandboxed {
|
||||
bail!("Cannot write in sandbox mode");
|
||||
}
|
||||
let volume = match ctx.volumes.get(&volume_id) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
bail!("There is no {} in volumes", volume_id);
|
||||
}
|
||||
};
|
||||
if volume.readonly() {
|
||||
bail!("Volume {} is readonly", volume_id);
|
||||
}
|
||||
let volume_path =
|
||||
volume.path_for(&ctx.datadir, &ctx.package_id, &ctx.version, &volume_id);
|
||||
let new_file = volume_path.join(path_in);
|
||||
let parent_new_file = new_file
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Expecting that file is not root"))?;
|
||||
// With the volume check
|
||||
if !is_subset(&volume_path, &parent_new_file).await? {
|
||||
bail!(
|
||||
"Path '{}' has broken away from parent '{}'",
|
||||
new_file.to_string_lossy(),
|
||||
volume_path.to_string_lossy(),
|
||||
);
|
||||
}
|
||||
tokio::fs::create_dir_all(new_file).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[op]
|
||||
fn current_function(state: &mut OpState) -> Result<String, AnyError> {
|
||||
let ctx = state.borrow::<JsContext>();
|
||||
Ok(ctx.run_function.clone())
|
||||
}
|
||||
|
||||
#[op]
|
||||
fn log_trace(state: &mut OpState, input: String) -> Result<(), AnyError> {
|
||||
let ctx = state.borrow::<JsContext>();
|
||||
tracing::trace!(
|
||||
package_id = tracing::field::display(&ctx.package_id),
|
||||
run_function = tracing::field::display(&ctx.run_function),
|
||||
"{}",
|
||||
input
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
#[op]
|
||||
fn log_warn(state: &mut OpState, input: String) -> Result<(), AnyError> {
|
||||
let ctx = state.borrow::<JsContext>();
|
||||
tracing::warn!(
|
||||
package_id = tracing::field::display(&ctx.package_id),
|
||||
run_function = tracing::field::display(&ctx.run_function),
|
||||
"{}",
|
||||
input
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
#[op]
|
||||
fn log_error(state: &mut OpState, input: String) -> Result<(), AnyError> {
|
||||
let ctx = state.borrow::<JsContext>();
|
||||
tracing::error!(
|
||||
package_id = tracing::field::display(&ctx.package_id),
|
||||
run_function = tracing::field::display(&ctx.run_function),
|
||||
"{}",
|
||||
input
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
#[op]
|
||||
fn log_debug(state: &mut OpState, input: String) -> Result<(), AnyError> {
|
||||
let ctx = state.borrow::<JsContext>();
|
||||
tracing::debug!(
|
||||
package_id = tracing::field::display(&ctx.package_id),
|
||||
run_function = tracing::field::display(&ctx.run_function),
|
||||
"{}",
|
||||
input
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
#[op]
|
||||
fn log_info(state: &mut OpState, input: String) -> Result<(), AnyError> {
|
||||
let ctx = state.borrow::<JsContext>();
|
||||
tracing::info!(
|
||||
package_id = tracing::field::display(&ctx.package_id),
|
||||
run_function = tracing::field::display(&ctx.run_function),
|
||||
"{}",
|
||||
input
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[op]
|
||||
fn get_context(state: &mut OpState) -> Result<JsContext, AnyError> {
|
||||
let ctx = state.borrow::<JsContext>();
|
||||
Ok(ctx.clone())
|
||||
}
|
||||
#[op]
|
||||
fn get_input(state: &mut OpState) -> Result<Value, AnyError> {
|
||||
let ctx = state.borrow::<JsContext>();
|
||||
Ok(ctx.input.clone())
|
||||
}
|
||||
#[op]
|
||||
fn set_value(state: &mut OpState, value: Value) -> Result<(), AnyError> {
|
||||
let mut answer = state.borrow::<AnswerState>().0.lock();
|
||||
*answer = value;
|
||||
Ok(())
|
||||
}
|
||||
#[op]
|
||||
fn is_sandboxed(state: &mut OpState) -> Result<bool, AnyError> {
|
||||
let ctx = state.borrow::<JsContext>();
|
||||
Ok(ctx.sandboxed)
|
||||
}
|
||||
|
||||
/// We need to make sure that during the file accessing, we don't reach beyond our scope of control
|
||||
async fn is_subset(
|
||||
parent: impl AsRef<Path>,
|
||||
child: impl AsRef<Path>,
|
||||
) -> Result<bool, AnyError> {
|
||||
let child = tokio::fs::canonicalize(child).await?;
|
||||
let parent = tokio::fs::canonicalize(parent).await?;
|
||||
Ok(child.starts_with(parent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn js_action_execute() {
|
||||
let js_action = JsProcedure {};
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,89 +0,0 @@
|
||||
//@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
|
||||
* @param {object} obj
|
||||
* @param {string} pointer
|
||||
* @returns
|
||||
*/
|
||||
function jsonPointerValue(obj, pointer) {
|
||||
const paths = pointer.substring(1).split("/");
|
||||
for (const path of paths) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
obj = (obj || {})[path];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const context = Deno.core.opSync("get_context");
|
||||
// @ts-ignore
|
||||
const writeFile = ({ path, volumeId, toWrite }) => Deno.core.opAsync("write_file", context, volumeId, path, toWrite);
|
||||
|
||||
// @ts-ignore
|
||||
const readFile = ({ volumeId, path }) => Deno.core.opAsync("read_file", context, volumeId, path);
|
||||
// @ts-ignore
|
||||
const removeFile = ({ volumeId, path }) => Deno.core.opAsync("remove_file", context, 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", context, volumeId, path);
|
||||
// @ts-ignore
|
||||
const removeDir = ({ volumeId, path }) => Deno.core.opAsync("remove_dir", context, 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,
|
||||
readFile,
|
||||
writeJsonFile,
|
||||
readJsonFile,
|
||||
error,
|
||||
warn,
|
||||
debug,
|
||||
trace,
|
||||
info,
|
||||
isSandboxed,
|
||||
removeFile,
|
||||
createDir,
|
||||
removeDir,
|
||||
};
|
||||
|
||||
const runFunction = jsonPointerValue(mainModule, currentFunction);
|
||||
(async () => {
|
||||
if (typeof runFunction !== "function") {
|
||||
error(`Expecting ${ currentFunction } to be a function`);
|
||||
throw new Error(`Expecting ${ currentFunction } to be a function`);
|
||||
}
|
||||
const answer = await runFunction(effects, input);
|
||||
setState(answer);
|
||||
})();
|
||||
@@ -6,66 +6,17 @@ use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
use self::docker::DockerProcedure;
|
||||
use self::js_scripts::JsProcedure;
|
||||
use crate::action::ActionId;
|
||||
use crate::context::RpcContext;
|
||||
use crate::id::ImageId;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::status::health_check::HealthCheckId;
|
||||
use crate::util::Version;
|
||||
use crate::volume::Volumes;
|
||||
use crate::Error;
|
||||
|
||||
pub mod docker;
|
||||
#[cfg(feature = "js_engine")]
|
||||
pub mod js_scripts;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProcedureName {
|
||||
Main, // Usually just run container
|
||||
CreateBackup,
|
||||
RestoreBackup,
|
||||
GetConfig,
|
||||
SetConfig,
|
||||
Migration,
|
||||
Properties,
|
||||
Check(PackageId),
|
||||
AutoConfig(PackageId),
|
||||
Health(HealthCheckId),
|
||||
Action(ActionId),
|
||||
}
|
||||
|
||||
impl ProcedureName {
|
||||
fn docker_name(&self) -> Option<String> {
|
||||
match self {
|
||||
ProcedureName::Main => None,
|
||||
ProcedureName::CreateBackup => Some("CreateBackup".to_string()),
|
||||
ProcedureName::RestoreBackup => Some("RestoreBackup".to_string()),
|
||||
ProcedureName::GetConfig => Some("GetConfig".to_string()),
|
||||
ProcedureName::SetConfig => Some("SetConfig".to_string()),
|
||||
ProcedureName::Migration => Some("Migration".to_string()),
|
||||
ProcedureName::Properties => Some(format!("Properties-{}", rand::random::<u64>())),
|
||||
ProcedureName::Health(id) => Some(format!("{}Health", id)),
|
||||
ProcedureName::Action(id) => Some(format!("{}Action", id)),
|
||||
ProcedureName::Check(_) => None,
|
||||
ProcedureName::AutoConfig(_) => None,
|
||||
}
|
||||
}
|
||||
fn js_function_name(&self) -> String {
|
||||
match self {
|
||||
ProcedureName::Main => "/main".to_string(),
|
||||
ProcedureName::CreateBackup => "/createBackup".to_string(),
|
||||
ProcedureName::RestoreBackup => "/restoreBackup".to_string(),
|
||||
ProcedureName::GetConfig => "/getConfig".to_string(),
|
||||
ProcedureName::SetConfig => "/setConfig".to_string(),
|
||||
ProcedureName::Migration => "/migration".to_string(),
|
||||
ProcedureName::Properties => "/properties".to_string(),
|
||||
ProcedureName::Health(id) => format!("/health/{}", id),
|
||||
ProcedureName::Action(id) => format!("/action/{}", id),
|
||||
ProcedureName::Check(id) => format!("/dependencies/{}/check", id),
|
||||
ProcedureName::AutoConfig(id) => format!("/dependencies/{}/autoConfigure", id),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use models::ProcedureName;
|
||||
|
||||
// TODO: create RPC endpoint that looks up the appropriate action and calls `execute`
|
||||
|
||||
@@ -74,11 +25,14 @@ impl ProcedureName {
|
||||
#[serde(tag = "type")]
|
||||
pub enum PackageProcedure {
|
||||
Docker(DockerProcedure),
|
||||
Script(JsProcedure),
|
||||
|
||||
#[cfg(feature = "js_engine")]
|
||||
Script(js_scripts::JsProcedure),
|
||||
}
|
||||
impl PackageProcedure {
|
||||
pub fn is_script(&self) -> bool {
|
||||
match self {
|
||||
#[cfg(feature = "js_engine")]
|
||||
Self::Script(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
@@ -93,6 +47,7 @@ impl PackageProcedure {
|
||||
match self {
|
||||
PackageProcedure::Docker(action) => action.validate(volumes, image_ids, expected_io),
|
||||
|
||||
#[cfg(feature = "js_engine")]
|
||||
PackageProcedure::Script(action) => action.validate(volumes),
|
||||
}
|
||||
}
|
||||
@@ -124,6 +79,7 @@ impl PackageProcedure {
|
||||
)
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "js_engine")]
|
||||
PackageProcedure::Script(procedure) => {
|
||||
procedure
|
||||
.execute(
|
||||
@@ -156,6 +112,7 @@ impl PackageProcedure {
|
||||
.sandboxed(ctx, pkg_id, pkg_version, volumes, input, timeout)
|
||||
.await
|
||||
}
|
||||
#[cfg(feature = "js_engine")]
|
||||
PackageProcedure::Script(procedure) => {
|
||||
procedure
|
||||
.sandboxed(ctx, pkg_id, pkg_version, volumes, input, timeout, name)
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use patch_db::HasModel;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::action::Actions;
|
||||
use crate::backup::BackupActions;
|
||||
use crate::config::action::ConfigActions;
|
||||
use crate::dependencies::Dependencies;
|
||||
use crate::id::{Id, InvalidId, SYSTEM_ID};
|
||||
use crate::migration::Migrations;
|
||||
use crate::net::interface::Interfaces;
|
||||
use crate::procedure::PackageProcedure;
|
||||
@@ -21,85 +18,7 @@ use crate::version::{Current, VersionT};
|
||||
use crate::volume::Volumes;
|
||||
use crate::Error;
|
||||
|
||||
pub const SYSTEM_PACKAGE_ID: PackageId<&'static str> = PackageId(SYSTEM_ID);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct PackageId<S: AsRef<str> = String>(Id<S>);
|
||||
impl<'a> PackageId<&'a str> {
|
||||
pub fn owned(&self) -> PackageId {
|
||||
PackageId(self.0.owned())
|
||||
}
|
||||
}
|
||||
impl FromStr for PackageId {
|
||||
type Err = InvalidId;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(PackageId(Id::try_from(s.to_owned())?))
|
||||
}
|
||||
}
|
||||
impl From<PackageId> for String {
|
||||
fn from(value: PackageId) -> Self {
|
||||
value.0.into()
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> From<Id<S>> for PackageId<S> {
|
||||
fn from(id: Id<S>) -> Self {
|
||||
PackageId(id)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::ops::Deref for PackageId<S> {
|
||||
type Target = S;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<PackageId<S>> for PackageId<S> {
|
||||
fn as_ref(&self) -> &PackageId<S> {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::fmt::Display for PackageId<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<str> for PackageId<S> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> Borrow<str> for PackageId<S> {
|
||||
fn borrow(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<Path> for PackageId<S> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de, S> Deserialize<'de> for PackageId<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
Id<S>: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
Ok(PackageId(Deserialize::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
impl<S> Serialize for PackageId<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
|
||||
where
|
||||
Ser: Serializer,
|
||||
{
|
||||
Serialize::serialize(&self.0, serializer)
|
||||
}
|
||||
}
|
||||
pub use models::{PackageId, SYSTEM_PACKAGE_ID};
|
||||
|
||||
fn current_version() -> Version {
|
||||
Current::new().semver().into()
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::id::{Id, ImageId};
|
||||
use crate::id::{ ImageId};
|
||||
use crate::procedure::{NoOutput, PackageProcedure, ProcedureName};
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::Duration;
|
||||
@@ -14,35 +13,7 @@ use crate::util::Version;
|
||||
use crate::volume::Volumes;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
pub struct HealthCheckId<S: AsRef<str> = String>(Id<S>);
|
||||
impl<S: AsRef<str>> std::fmt::Display for HealthCheckId<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<str> for HealthCheckId<S> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de, S> Deserialize<'de> for HealthCheckId<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
Id<S>: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(HealthCheckId(Deserialize::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<Path> for HealthCheckId<S> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
pub use models::HealthCheckId;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct HealthChecks(pub BTreeMap<HealthCheckId, HealthCheck>);
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::process::Stdio;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use ::serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use async_trait::async_trait;
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::{self, eyre};
|
||||
@@ -19,16 +15,17 @@ use fd_lock_rs::FdLock;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
use lazy_static::lazy_static;
|
||||
use patch_db::{HasModel, Model};
|
||||
use pin_project::pin_project;
|
||||
use tokio::fs::File;
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard, RwLock};
|
||||
use tokio::task::{JoinError, JoinHandle};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::{Error, ResultExt as _};
|
||||
|
||||
|
||||
pub use helpers::NonDetachingJoinHandle;
|
||||
pub use models::Version;
|
||||
pub mod io;
|
||||
pub mod logger;
|
||||
pub mod serde;
|
||||
@@ -125,110 +122,6 @@ impl<T> SNone<T> {
|
||||
}
|
||||
impl<T> SOption<T> for SNone<T> {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Version {
|
||||
version: emver::Version,
|
||||
string: String,
|
||||
}
|
||||
impl Version {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.string.as_str()
|
||||
}
|
||||
pub fn into_version(self) -> emver::Version {
|
||||
self.version
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.string)
|
||||
}
|
||||
}
|
||||
impl std::str::FromStr for Version {
|
||||
type Err = <emver::Version as FromStr>::Err;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Version {
|
||||
string: s.to_owned(),
|
||||
version: s.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl From<emver::Version> for Version {
|
||||
fn from(v: emver::Version) -> Self {
|
||||
Version {
|
||||
string: v.to_string(),
|
||||
version: v,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Version> for emver::Version {
|
||||
fn from(v: Version) -> Self {
|
||||
v.version
|
||||
}
|
||||
}
|
||||
impl Default for Version {
|
||||
fn default() -> Self {
|
||||
Self::from(emver::Version::default())
|
||||
}
|
||||
}
|
||||
impl Deref for Version {
|
||||
type Target = emver::Version;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.version
|
||||
}
|
||||
}
|
||||
impl AsRef<emver::Version> for Version {
|
||||
fn as_ref(&self) -> &emver::Version {
|
||||
&self.version
|
||||
}
|
||||
}
|
||||
impl AsRef<str> for Version {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
impl PartialEq for Version {
|
||||
fn eq(&self, other: &Version) -> bool {
|
||||
self.version.eq(&other.version)
|
||||
}
|
||||
}
|
||||
impl Eq for Version {}
|
||||
impl PartialOrd for Version {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.version.partial_cmp(&other.version)
|
||||
}
|
||||
}
|
||||
impl Ord for Version {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.version.cmp(&other.version)
|
||||
}
|
||||
}
|
||||
impl Hash for Version {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.version.hash(state)
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for Version {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let string = String::deserialize(deserializer)?;
|
||||
let version = emver::Version::from_str(&string).map_err(::serde::de::Error::custom)?;
|
||||
Ok(Self { string, version })
|
||||
}
|
||||
}
|
||||
impl Serialize for Version {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.string.serialize(serializer)
|
||||
}
|
||||
}
|
||||
impl HasModel for Version {
|
||||
type Model = Model<Version>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait AsyncFileExt: Sized {
|
||||
async fn maybe_open<P: AsRef<Path> + Send + Sync>(path: P) -> std::io::Result<Option<Self>>;
|
||||
@@ -358,30 +251,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project(PinnedDrop)]
|
||||
pub struct NonDetachingJoinHandle<T>(#[pin] JoinHandle<T>);
|
||||
impl<T> From<JoinHandle<T>> for NonDetachingJoinHandle<T> {
|
||||
fn from(t: JoinHandle<T>) -> Self {
|
||||
NonDetachingJoinHandle(t)
|
||||
}
|
||||
}
|
||||
#[pin_project::pinned_drop]
|
||||
impl<T> PinnedDrop for NonDetachingJoinHandle<T> {
|
||||
fn drop(self: std::pin::Pin<&mut Self>) {
|
||||
let this = self.project();
|
||||
this.0.into_ref().get_ref().abort()
|
||||
}
|
||||
}
|
||||
impl<T> Future for NonDetachingJoinHandle<T> {
|
||||
type Output = Result<T, JoinError>;
|
||||
fn poll(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
this.0.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GeneralGuard<F: FnOnce() -> T, T = ()>(Option<F>);
|
||||
impl<F: FnOnce() -> T, T> GeneralGuard<F, T> {
|
||||
|
||||
@@ -11,8 +11,9 @@ mod v0_3_0;
|
||||
mod v0_3_0_1;
|
||||
mod v0_3_0_2;
|
||||
mod v0_3_0_3;
|
||||
mod v0_3_1;
|
||||
|
||||
pub type Current = v0_3_0_3::Version;
|
||||
pub type Current = v0_3_1::Version;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
@@ -21,6 +22,7 @@ enum Version {
|
||||
V0_3_0_1(Wrapper<v0_3_0_1::Version>),
|
||||
V0_3_0_2(Wrapper<v0_3_0_2::Version>),
|
||||
V0_3_0_3(Wrapper<v0_3_0_3::Version>),
|
||||
V0_3_1(Wrapper<v0_3_1::Version>),
|
||||
Other(emver::Version),
|
||||
}
|
||||
|
||||
@@ -40,6 +42,7 @@ impl Version {
|
||||
Version::V0_3_0_1(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_0_2(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_0_3(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_1(Wrapper(x)) => x.semver(),
|
||||
Version::Other(x) => x.clone(),
|
||||
}
|
||||
}
|
||||
@@ -154,6 +157,7 @@ pub async fn init<Db: DbHandle>(
|
||||
Version::V0_3_0_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_0_2(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_0_3(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::Other(_) => {
|
||||
return Err(Error::new(
|
||||
eyre!("Cannot downgrade"),
|
||||
@@ -189,6 +193,7 @@ mod tests {
|
||||
Just(Version::V0_3_0_1(Wrapper(v0_3_0_1::Version::new()))),
|
||||
Just(Version::V0_3_0_2(Wrapper(v0_3_0_2::Version::new()))),
|
||||
Just(Version::V0_3_0_3(Wrapper(v0_3_0_3::Version::new()))),
|
||||
Just(Version::V0_3_1(Wrapper(v0_3_1::Version::new()))),
|
||||
em_version().prop_map(Version::Other),
|
||||
]
|
||||
}
|
||||
|
||||
36
backend/src/version/v0_3_1.rs
Normal file
36
backend/src/version/v0_3_1.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use emver::VersionRange;
|
||||
|
||||
use super::*;
|
||||
|
||||
const V0_3_1: emver::Version = emver::Version::new(0, 3, 1, 0);
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref V0_3_1_COMPAT: VersionRange = VersionRange::Conj(
|
||||
Box::new(VersionRange::Anchor(
|
||||
emver::GTE,
|
||||
emver::Version::new(0, 3, 0, 0),
|
||||
)),
|
||||
Box::new(VersionRange::Anchor(emver::LTE, Current::new().semver())),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Version;
|
||||
#[async_trait]
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_3_0_3::Version;
|
||||
fn new() -> Self {
|
||||
Version
|
||||
}
|
||||
fn semver(&self) -> emver::Version {
|
||||
V0_3_1
|
||||
}
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_1_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,23 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use patch_db::{HasModel, Map, MapModel};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::id::{Id, IdUnchecked};
|
||||
use crate::install::PKG_SCRIPT_DIR;
|
||||
use crate::net::interface::{InterfaceId, Interfaces};
|
||||
use crate::net::NetController;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::Version;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub const PKG_VOLUME_DIR: &'static str = "package-data/volumes";
|
||||
pub const BACKUP_DIR: &'static str = "/media/embassy-os/backups";
|
||||
pub use helpers::script_dir;
|
||||
pub use models::VolumeId;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum VolumeId<S: AsRef<str> = String> {
|
||||
Backup,
|
||||
Custom(Id<S>),
|
||||
}
|
||||
impl<S: AsRef<str>> std::fmt::Display for VolumeId<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VolumeId::Backup => write!(f, "BACKUP"),
|
||||
VolumeId::Custom(id) => write!(f, "{}", id),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<str> for VolumeId<S> {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
VolumeId::Backup => "BACKUP",
|
||||
VolumeId::Custom(id) => id.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> Borrow<str> for VolumeId<S> {
|
||||
fn borrow(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<Path> for VolumeId<S> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
AsRef::<str>::as_ref(self).as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de, S> Deserialize<'de> for VolumeId<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
IdUnchecked<S>: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let unchecked: IdUnchecked<S> = Deserialize::deserialize(deserializer)?;
|
||||
Ok(match unchecked.0.as_ref() {
|
||||
"BACKUP" => VolumeId::Backup,
|
||||
_ => VolumeId::Custom(Id::try_from(unchecked.0).map_err(serde::de::Error::custom)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> Serialize for VolumeId<S> {
|
||||
fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
|
||||
where
|
||||
Ser: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_ref())
|
||||
}
|
||||
}
|
||||
pub const PKG_VOLUME_DIR: &str = "package-data/volumes";
|
||||
pub const BACKUP_DIR: &str = "/media/embassy-os/backups";
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Volumes(BTreeMap<VolumeId, Volume>);
|
||||
@@ -166,14 +110,6 @@ pub fn asset_dir<P: AsRef<Path>>(datadir: P, pkg_id: &PackageId, version: &Versi
|
||||
.join(version.as_str())
|
||||
}
|
||||
|
||||
pub fn script_dir<P: AsRef<Path>>(datadir: P, pkg_id: &PackageId, version: &Version) -> PathBuf {
|
||||
datadir
|
||||
.as_ref()
|
||||
.join(&*PKG_SCRIPT_DIR)
|
||||
.join(pkg_id)
|
||||
.join(version.as_str())
|
||||
}
|
||||
|
||||
pub fn backup_dir(pkg_id: &PackageId) -> PathBuf {
|
||||
Path::new(BACKUP_DIR).join(pkg_id).join("data")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user