fix(deploy): Ignore error if /opt/gbo/bin/botserver doesn't exist on first deploy
All checks were successful
BotServer CI/CD / build (push) Successful in 5m7s

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-04-05 19:38:54 -03:00
parent 86bb4cad8e
commit 15d9e3c142
3 changed files with 134 additions and 28 deletions

View file

@ -93,8 +93,8 @@ jobs:
echo "=== Deploy started ===" echo "=== Deploy started ==="
echo "Step 1: Checking binary..." echo "Step 1: Checking binary..."
ls -lh /opt/gbo/data/botserver/target/debug/botserver ls -lh /opt/gbo/data/botserver/target/debug/botserver
echo "Step 2: Backing up old binary..." echo "Step 2: Backing up old binary (ignore if not exists)..."
ssh $SSH_ARGS system "cp /opt/gbo/bin/botserver /tmp/botserver.bak" ssh $SSH_ARGS system "cp /opt/gbo/bin/botserver /tmp/botserver.bak 2>/dev/null || true"
echo "Step 3: Stopping botserver service..." echo "Step 3: Stopping botserver service..."
ssh $SSH_ARGS system "sudo systemctl stop botserver || true" ssh $SSH_ARGS system "sudo systemctl stop botserver || true"
echo "Step 4: Transferring new binary..." echo "Step 4: Transferring new binary..."

View file

@ -1,9 +1,12 @@
use crate::core::shared::models::UserSession; use crate::core::shared::models::UserSession;
use crate::core::shared::state::AppState; use crate::core::shared::state::AppState;
use crate::core::shared::utils;
use botlib::MAX_LOOP_ITERATIONS; use botlib::MAX_LOOP_ITERATIONS;
use diesel::prelude::*;
use log::trace; use log::trace;
use rhai::{Dynamic, Engine}; use rhai::{Dynamic, Engine};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -17,10 +20,10 @@ pub struct ProcedureDefinition {
static PROCEDURES: std::sync::LazyLock<Arc<Mutex<HashMap<String, ProcedureDefinition>>>> = static PROCEDURES: std::sync::LazyLock<Arc<Mutex<HashMap<String, ProcedureDefinition>>>> =
std::sync::LazyLock::new(|| Arc::new(Mutex::new(HashMap::new()))); std::sync::LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
pub fn register_procedure_keywords(_state: Arc<AppState>, _user: UserSession, engine: &mut Engine) { pub fn register_procedure_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
register_while_wend(engine); register_while_wend(engine);
register_do_loop(engine); register_do_loop(engine);
register_call_keyword(engine); register_call_keyword(state, user, engine);
register_return_keyword(engine); register_return_keyword(engine);
} }
@ -258,38 +261,42 @@ fn eval_bool_condition(value: &Dynamic) -> bool {
} }
} }
fn register_call_keyword(engine: &mut Engine) { fn register_call_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine engine
.register_custom_syntax( .register_custom_syntax(
["CALL", "$ident$", "(", "$expr$", ")"], ["CALL", "$ident$", "(", "$expr$", ")"],
false, false,
|context, inputs| { move |context, inputs| {
let proc_name = inputs[0] let proc_name = inputs[0]
.get_string_value() .get_string_value()
.unwrap_or_default() .unwrap_or_default()
.to_uppercase(); .to_uppercase();
let args = context.eval_expression_tree(&inputs[1])?; let _args = context.eval_expression_tree(&inputs[1])?;
trace!("CALL {} with args: {:?}", proc_name, args); trace!("CALL {} with args", proc_name);
let procedures = PROCEDURES.lock().expect("mutex not poisoned"); // Check for in-memory procedure first
if let Some(proc) = procedures.get(&proc_name) { {
trace!( let procedures = PROCEDURES.lock().expect("mutex not poisoned");
"Found procedure: {} (is_function: {})", if procedures.contains_key(&proc_name) {
proc.name, return Ok(Dynamic::UNIT);
proc.is_function }
);
Ok(Dynamic::UNIT)
} else {
Err(format!("Undefined procedure: {}", proc_name).into())
} }
// Try to execute as .bas file
call_bas_script(&state_clone, &user_clone, &proc_name)
}, },
) )
.expect("Failed to register CALL with args syntax"); .expect("Failed to register CALL with args syntax");
let state_clone2 = Arc::clone(&state);
let user_clone2 = user.clone();
engine engine
.register_custom_syntax(["CALL", "$ident$"], false, |_context, inputs| { .register_custom_syntax(["CALL", "$ident$"], false, move |_context, inputs| {
let proc_name = inputs[0] let proc_name = inputs[0]
.get_string_value() .get_string_value()
.unwrap_or_default() .unwrap_or_default()
@ -297,16 +304,84 @@ fn register_call_keyword(engine: &mut Engine) {
trace!("CALL {} (no args)", proc_name); trace!("CALL {} (no args)", proc_name);
let procedures = PROCEDURES.lock().expect("mutex not poisoned"); // Check for in-memory procedure first
if procedures.contains_key(&proc_name) { {
Ok(Dynamic::UNIT) let procedures = PROCEDURES.lock().expect("mutex not poisoned");
} else { if procedures.contains_key(&proc_name) {
Err(format!("Undefined procedure: {}", proc_name).into()) return Ok(Dynamic::UNIT);
}
} }
// Try to execute as .bas file
call_bas_script(&state_clone2, &user_clone2, &proc_name)
}) })
.expect("Failed to register CALL without args syntax"); .expect("Failed to register CALL without args syntax");
} }
fn call_bas_script(state: &Arc<AppState>, user: &UserSession, script_name: &str) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
// Get bot name from bot_id
let bot_name = {
if let Some(mut conn) = state.conn.get().ok() {
use crate::core::shared::models::schema::bots::dsl::*;
bots.filter(id.eq(user.bot_id))
.select(name)
.first::<String>(&mut *conn)
.unwrap_or_else(|_| "default".to_string())
} else {
"default".to_string()
}
};
let work_path = utils::get_work_path();
let script_path = PathBuf::from(&work_path)
.join(format!("{}.gbai", bot_name))
.join(format!("{}.gbdialog", bot_name))
.join(format!("{}.bas", script_name.to_lowercase()));
if !script_path.exists() {
return Err(format!("Undefined procedure/script: {}", script_name).into());
}
let script_content = match std::fs::read_to_string(&script_path) {
Ok(c) => c,
Err(e) => return Err(format!("Failed to read script {}: {}", script_name, e).into()),
};
trace!("Executing .bas script: {:?}", script_path);
// Clone necessary data for thread
let state_clone = state.clone();
let user_clone = user.clone();
// Use blocking channel for thread communication
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build();
if let Ok(rt) = rt {
let result = rt.block_on(async {
crate::basic::ScriptService::execute_script(
state_clone,
user_clone,
&script_content,
).await
});
let _ = tx.send(result);
} else {
let _ = tx.send(Err("Failed to create runtime".into()));
}
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(Ok(result)) => Ok(Dynamic::from(result)),
Ok(Err(e)) => Err(format!("Script error: {}", e).into()),
Err(_) => Err("Script execution timed out".into()),
}
}
fn register_return_keyword(engine: &mut Engine) { fn register_return_keyword(engine: &mut Engine) {
engine engine
.register_custom_syntax(["RETURN", "$expr$"], false, |context, inputs| { .register_custom_syntax(["RETURN", "$expr$"], false, |context, inputs| {
@ -371,7 +446,10 @@ pub fn preprocess_subs(input: &str) -> String {
}; };
trace!("Registering SUB: {}", sub_name); trace!("Registering SUB: {}", sub_name);
PROCEDURES.lock().expect("mutex not poisoned").insert(sub_name.clone(), proc); PROCEDURES
.lock()
.expect("mutex not poisoned")
.insert(sub_name.clone(), proc);
sub_name.clear(); sub_name.clear();
sub_params.clear(); sub_params.clear();
@ -445,7 +523,10 @@ pub fn preprocess_functions(input: &str) -> String {
}; };
trace!("Registering FUNCTION: {}", func_name); trace!("Registering FUNCTION: {}", func_name);
PROCEDURES.lock().expect("mutex not poisoned").insert(func_name.clone(), proc); PROCEDURES
.lock()
.expect("mutex not poisoned")
.insert(func_name.clone(), proc);
func_name.clear(); func_name.clear();
func_params.clear(); func_params.clear();
@ -538,7 +619,12 @@ pub fn clear_procedures() {
} }
pub fn get_procedure_names() -> Vec<String> { pub fn get_procedure_names() -> Vec<String> {
PROCEDURES.lock().expect("mutex not poisoned").keys().cloned().collect() PROCEDURES
.lock()
.expect("mutex not poisoned")
.keys()
.cloned()
.collect()
} }
pub fn has_procedure(name: &str) -> bool { pub fn has_procedure(name: &str) -> bool {

View file

@ -632,6 +632,26 @@ impl ScriptService {
self.engine.eval_ast_with_scope(&mut self.scope, ast) self.engine.eval_ast_with_scope(&mut self.scope, ast)
} }
/// Execute a BASIC script asynchronously
pub async fn execute_script(
state: Arc<AppState>,
user: UserSession,
script: &str,
) -> Result<String, String> {
let mut script_service = Self::new(state.clone(), user.clone());
script_service.load_bot_config_params(&state, user.bot_id);
match script_service.compile(script) {
Ok(ast) => {
match script_service.run(&ast) {
Ok(result) => Ok(result.to_string()),
Err(e) => Err(format!("Execution error: {}", e)),
}
}
Err(e) => Err(format!("Compilation error: {}", e)),
}
}
/// Convert SAVE statements for tool compilation (simplified, no DB lookup) /// Convert SAVE statements for tool compilation (simplified, no DB lookup)
/// SAVE "table", var1, var2, ... -> let __data__ = #{var1: var1, var2: var2, ...}; SAVE "table", __data__ /// SAVE "table", var1, var2, ... -> let __data__ = #{var1: var1, var2: var2, ...}; SAVE "table", __data__
fn convert_save_for_tools(script: &str) -> String { fn convert_save_for_tools(script: &str) -> String {