blob: d95feae6f61a8a20315c5b7b43a6779775873cde [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::{
protocol::{Reply, Request, Start},
value::{from_json, to_json},
};
use maka_code_runtime::{CellDiagnostic, CellDiagnosticKind};
use monty::{MontyRun, RunProgress};
use monty_types::{
CompileOptions, ExcType, ExtFunctionResult, MontyException, MontyObject, MontyUuid,
NameLookupResult, OsFunctionCall, OsPolicy, PrintWriter, ResourceLimits, ResourceTracker,
SleepMode,
};
use std::{
io::{self, BufRead, Write},
time::Duration,
};
const HELPERS: &[&str] = &[
"text",
"image",
"audio",
"generated_image",
"notify",
"store",
"load",
"yield_control",
"exit",
];
/// Internal entry point of the bundled worker executable. Its allocator must be
/// monty_alloc::LimitedAllocator; never install that allocator in the Host.
pub fn run_stdio() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let heap: usize = args.next().ok_or("missing heap budget")?.parse()?;
let timeout: u64 = args.next().ok_or("missing execution budget")?.parse()?;
monty_alloc::set_hard_limit(monty_types::memory_limit_with_headroom(Some(heap), false))?;
let stdin = io::stdin();
let stdout = io::stdout();
let mut input = stdin.lock();
let mut output = stdout.lock();
let start: Start = read(&mut input)?;
write(&mut output, &Request::Ready)?;
let result = execute(
start,
ResourceLimits {
max_feed_duration: Some(Duration::from_millis(timeout)),
max_memory: Some(heap),
..Default::default()
},
&mut input,
&mut output,
);
let error = match result {
Ok(()) => None,
Err(WorkerError::Python(error)) => Some(diagnostic(error)),
Err(WorkerError::Diagnostic(error)) => Some(error),
Err(WorkerError::Protocol(error)) => return Err(error),
};
write(&mut output, &Request::Complete { error })?;
Ok(())
}
enum WorkerError {
Diagnostic(CellDiagnostic),
Python(MontyException),
Protocol(Box<dyn std::error::Error>),
}
impl From<MontyException> for WorkerError {
fn from(error: MontyException) -> Self {
Self::Python(error)
}
}
impl From<Box<dyn std::error::Error>> for WorkerError {
fn from(error: Box<dyn std::error::Error>) -> Self {
Self::Protocol(error)
}
}
fn execute(
start: Start,
limits: ResourceLimits,
input: &mut impl BufRead,
output: &mut impl Write,
) -> Result<(), WorkerError> {
let mut inputs: Vec<_> = HELPERS
.iter()
.map(|name| MontyObject::function(format!("helper:{name}"), None))
.collect();
let mut names: Vec<String> = HELPERS.iter().map(|name| (*name).into()).collect();
names.extend(["tools".into(), "ALL_TOOLS".into()]);
inputs.push(MontyObject::module(
"tools",
MontyUuid::from_bytes(*uuid::Uuid::new_v4().as_bytes()),
start.names.into_iter().map(|name| {
(
MontyObject::string(crate::tool_identifier(&name)),
MontyObject::function(format!("tool:{name}"), None),
)
}),
));
inputs.push(MontyObject::tuple(start.metadata.into_iter().map(|tool| {
MontyObject::named_tuple(
"Tool",
["name", "description"],
[
MontyObject::string(tool.name),
MontyObject::string(tool.description),
],
)
})));
let runner = match MontyRun::new(
start.source,
"code_mode.py",
names,
CompileOptions::default(),
) {
Ok(runner) => runner.with_os_policy(OsPolicy {
sleep: SleepMode::CallHost,
..Default::default()
}),
Err(error) => {
return Err(WorkerError::Diagnostic(CellDiagnostic::new(
CellDiagnosticKind::ParseError,
error.to_string(),
)));
}
};
let mut progress = runner.start(inputs, ResourceTracker::new(limits), PrintWriter::Disabled)?;
loop {
progress = match progress {
RunProgress::Complete(_) => return Ok(()),
RunProgress::FunctionCall(call) => {
let arguments = (|| -> Result<_, String> {
let args = call.args.args().map(to_json).collect::<Result<_, _>>()?;
let kwargs = call
.args
.kwargs()
.map(|(key, value)| {
Ok((
key.as_str().ok_or("keyword must be a string")?.into(),
to_json(value)?,
))
})
.collect::<Result<_, String>>()?;
Ok((args, kwargs))
})();
let reply = match arguments {
Ok((args, kwargs)) => exchange(
input,
output,
&Request::Call {
id: call.call_id,
name: call.function_name.clone(),
args,
kwargs,
},
)?,
Err(error) => Reply::Error {
error: CellDiagnostic::new(CellDiagnosticKind::ExecutionError, error),
},
};
match reply {
Reply::Pending => call.resume_pending(PrintWriter::Disabled)?,
reply => call.resume(reply_result(reply)?, PrintWriter::Disabled)?,
}
}
RunProgress::ResolveFutures(wait) => {
let reply = exchange(
input,
output,
&Request::Resolve {
ids: wait.pending_call_ids().to_vec(),
},
)?;
let Reply::Resolved { results } = reply else {
return Err(protocol("expected resolved futures"));
};
wait.resume(
results
.into_iter()
.map(|(id, result)| (id, external(result)))
.collect(),
PrintWriter::Disabled,
)?
}
RunProgress::OsCall(call) => {
let (duration, asynchronous) = match call.function_call {
OsFunctionCall::Sleep(duration) => (duration, false),
OsFunctionCall::AsyncSleep(duration) => (duration, true),
_ => {
progress = call.resume(
MontyException::runtime_error(
"direct OS access is unavailable; use an authorized Host tool",
),
PrintWriter::Disabled,
)?;
continue;
}
};
let reply = exchange(
input,
output,
&Request::Sleep {
id: call.call_id,
duration,
asynchronous,
},
)?;
let result = match reply {
Reply::Pending => ExtFunctionResult::Future(call.call_id),
reply => reply_result(reply)?,
};
call.resume(result, PrintWriter::Disabled)?
}
RunProgress::NameLookup(lookup) => {
lookup.resume(NameLookupResult::Undefined, PrintWriter::Disabled)?
}
};
}
}
fn external(result: Result<serde_json::Value, CellDiagnostic>) -> ExtFunctionResult {
match result {
Ok(value) => ExtFunctionResult::Return(from_json(value)),
Err(error) => ExtFunctionResult::Error(MontyException::runtime_error(error.message)),
}
}
fn reply_result(reply: Reply) -> Result<ExtFunctionResult, WorkerError> {
match reply {
Reply::Return { value } => Ok(external(Ok(value))),
Reply::Error { error } => Ok(external(Err(error))),
_ => Err(protocol("expected function result")),
}
}
fn protocol(message: &str) -> WorkerError {
WorkerError::Protocol(message.into())
}
fn exchange(
input: &mut impl BufRead,
output: &mut impl Write,
request: &Request,
) -> Result<Reply, Box<dyn std::error::Error>> {
write(output, request)?;
read(input)
}
fn read<T: serde::de::DeserializeOwned>(
input: &mut impl BufRead,
) -> Result<T, Box<dyn std::error::Error>> {
let mut line = String::new();
if input.read_line(&mut line)? == 0 {
return Err("Host closed the worker protocol".into());
}
Ok(crate::protocol::decode(line.as_bytes())?)
}
fn write(
output: &mut impl Write,
message: &impl serde::Serialize,
) -> Result<(), Box<dyn std::error::Error>> {
serde_json::to_writer(&mut *output, message)?;
output.write_all(b"\n")?;
output.flush()?;
Ok(())
}
fn diagnostic(error: MontyException) -> CellDiagnostic {
let kind = match error.exc_type() {
ExcType::MemoryError | ExcType::TimeoutError => CellDiagnosticKind::LimitExceeded,
_ => CellDiagnosticKind::ExecutionError,
};
CellDiagnostic::new(kind, error.to_string())
}