Skip to content

Commit fd9c2a4

Browse files
authored
Merge pull request #170 from Sapphillon/169-fix-ext-plugin-server
169 fix ext plugin server
2 parents 066194b + ba7b089 commit fd9c2a4

File tree

3 files changed

+169
-14
lines changed

3 files changed

+169
-14
lines changed

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"Swellaby.vscode-rust-test-adapter",
1616
"GitHub.copilot-chat",
1717
"GitHub.copilot",
18-
"RooVeterinaryInc.roo-cline"
18+
"RooVeterinaryInc.roo-cline",
19+
"kilocode.kilo-code"
1920
]
2021
}
2122
}
Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1-
use ext_plugin::extplugin_server;
1+
use ext_plugin::{ExternalPluginRunRequest, ExternalPluginRunResponse, extplugin_server};
2+
use ipc_channel::ipc::{self, IpcSender};
23
use std::env;
34

4-
fn main() -> anyhow::Result<()> {
5+
#[tokio::main]
6+
async fn main() -> anyhow::Result<()> {
57
let args: Vec<String> = env::args().collect();
68
if args.len() < 2 {
79
anyhow::bail!("Usage: extplugin_test_server <server_name>");
810
}
911
let server_name = &args[1];
10-
extplugin_server(server_name)?;
12+
13+
if env::var("SAPPHILLON_TEST_SERVER_ABORT").is_ok() {
14+
let (tx_req, rx_req) = ipc::channel()?;
15+
let tx_bootstrap: IpcSender<
16+
IpcSender<(
17+
IpcSender<ExternalPluginRunResponse>,
18+
ExternalPluginRunRequest,
19+
)>,
20+
> = IpcSender::connect(server_name.to_string())?;
21+
tx_bootstrap.send(tx_req.clone())?;
22+
std::mem::forget(tx_bootstrap);
23+
std::mem::forget(tx_req);
24+
25+
if let Ok((_tx_res, _request)) = rx_req.recv() {
26+
std::process::exit(1);
27+
}
28+
std::process::exit(1);
29+
}
30+
31+
extplugin_server(server_name).await?;
1132
Ok(())
1233
}

ext_plugin/src/extplugin_runner_process.rs

Lines changed: 143 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,35 @@ pub fn extplugin_client(
9494

9595
tx_req.send((tx_res.clone(), request))?;
9696

97-
let response = rx_res.recv()?;
97+
// Wait for either the response or the server process termination
98+
let response = loop {
99+
if let Some(status) = child.try_wait()? {
100+
if !status.success() {
101+
anyhow::bail!("Server process terminated abnormally");
102+
} else {
103+
// サーバーは応答を送信せずに正常に終了しました。
104+
// 決して届かないメッセージを無期限に待つのを避けます。
105+
anyhow::bail!("Server process exited successfully without sending a response");
106+
}
107+
}
108+
109+
match rx_res.try_recv_timeout(std::time::Duration::from_millis(100)) {
110+
Ok(resp) => break resp,
111+
Err(ipc::TryRecvError::Empty) => continue,
112+
Err(ipc::TryRecvError::IpcError(err)) => anyhow::bail!(err),
113+
}
114+
};
98115

99-
let _ = child.kill();
100-
let _ = child.wait();
116+
let mut killed = false;
117+
if child.try_wait()?.is_none() {
118+
let _ = child.kill();
119+
killed = true;
120+
}
121+
// TODO: Improve error handling for server process termination
122+
let exit_status = child.wait()?;
123+
if !exit_status.success() && !killed {
124+
anyhow::bail!("Server process terminated abnormally: {exit_status:?}");
125+
}
101126

102127
if let Some(err) = response.error_message {
103128
anyhow::bail!(err);
@@ -106,7 +131,7 @@ pub fn extplugin_client(
106131
RsJsBridgeReturns::new_from_str(&response.returns_json)
107132
}
108133

109-
pub fn extplugin_server(server_name: &str) -> Result<()> {
134+
pub async fn extplugin_server(server_name: &str) -> Result<()> {
110135
use crate::permissions::permissions_options_from_sapphillon_permissions;
111136

112137
let (tx_req, rx_req) = ipc::channel()?;
@@ -122,10 +147,6 @@ pub fn extplugin_server(server_name: &str) -> Result<()> {
122147
}
123148
std::mem::forget(tx_req);
124149

125-
let rt = tokio::runtime::Builder::new_current_thread()
126-
.enable_all()
127-
.build()?;
128-
129150
if let Ok((tx_res, request)) = rx_req.recv() {
130151
// Convert IpcPermission back to proto::Permission
131152
let sapphillon_permissions: Vec<proto::sapphillon::v1::Permission> = request
@@ -142,11 +163,12 @@ pub fn extplugin_server(server_name: &str) -> Result<()> {
142163
Some(permissions_options)
143164
};
144165

145-
let result = rt.block_on(async {
166+
let result = async {
146167
let package = SapphillonPackage::new_async(&request.package_js).await?;
147168
let args = RsJsBridgeArgs::new_from_str(&request.args_json)?;
148169
package.execute(args, &permissions_options).await
149-
});
170+
}
171+
.await;
150172

151173
let response = match result {
152174
Ok(returns) => ExternalPluginRunResponse {
@@ -169,9 +191,20 @@ pub fn extplugin_server(server_name: &str) -> Result<()> {
169191
mod tests {
170192
use super::*;
171193
use serde_json::json;
194+
use std::sync::{Mutex, OnceLock};
195+
196+
static TEST_SERVER_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
197+
198+
fn test_server_lock() -> std::sync::MutexGuard<'static, ()> {
199+
TEST_SERVER_MUTEX
200+
.get_or_init(|| Mutex::new(()))
201+
.lock()
202+
.unwrap_or_else(|err| err.into_inner())
203+
}
172204

173205
#[test]
174206
fn test_extplugin_runner_process() -> Result<()> {
207+
let _guard = test_server_lock();
175208
let package_script = r#"
176209
globalThis.Sapphillon = {
177210
Package: {
@@ -203,6 +236,10 @@ mod tests {
203236
}
204237
};
205238
"#;
239+
if std::env::var("SAPPHILLON_TEST_SERVER_ABORT").is_ok() {
240+
// Skip this test if the abort environment variable is set
241+
return Ok(());
242+
}
206243

207244
let package = SapphillonPackage::new(package_script)?;
208245

@@ -255,4 +292,100 @@ mod tests {
255292

256293
Ok(())
257294
}
295+
296+
#[test]
297+
fn test_extplugin_runner_process_abnormal_exit_returns_error() -> Result<()> {
298+
let _guard = test_server_lock();
299+
let package_script = r#"
300+
globalThis.Sapphillon = {
301+
Package: {
302+
meta: {
303+
name: "test-plugin",
304+
version: "1.0.0",
305+
description: "Test plugin",
306+
author_id: "com.example",
307+
package_id: "com.example.test-plugin"
308+
},
309+
functions: {
310+
echo: {
311+
description: "Echoes the input",
312+
permissions: [],
313+
parameters: [{
314+
idx: 0,
315+
name: "message",
316+
type: "string",
317+
description: "Message to echo"
318+
}],
319+
returns: [{
320+
idx: 0,
321+
type: "string",
322+
description: "Echoed message"
323+
}],
324+
handler: (message) => message
325+
}
326+
}
327+
}
328+
};
329+
"#;
330+
331+
let package = SapphillonPackage::new(package_script)?;
332+
333+
let args = RsJsBridgeArgs {
334+
func_name: "echo".to_string(),
335+
args: vec![("message".to_string(), json!("Hello, World!"))]
336+
.into_iter()
337+
.collect(),
338+
};
339+
340+
let mut server_path_buf = std::env::current_exe()?;
341+
server_path_buf.pop();
342+
if server_path_buf.file_name().and_then(|s| s.to_str()) == Some("deps") {
343+
server_path_buf.pop();
344+
}
345+
server_path_buf.push("extplugin_test_server");
346+
347+
if !server_path_buf.exists() {
348+
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
349+
let status = std::process::Command::new("cargo")
350+
.args(["build", "--bin", "extplugin_test_server"])
351+
.current_dir(&manifest_dir)
352+
.status()
353+
.expect("Failed to execute cargo build");
354+
if !status.success() {
355+
anyhow::bail!("Failed to build extplugin_test_server");
356+
}
357+
}
358+
359+
let server_path = server_path_buf
360+
.to_str()
361+
.ok_or_else(|| anyhow::anyhow!("Invalid path"))?;
362+
let server_args = vec![];
363+
let sapphillon_permissions = vec![];
364+
365+
unsafe {
366+
std::env::set_var("SAPPHILLON_TEST_SERVER_ABORT", "1");
367+
}
368+
let result = extplugin_client(
369+
&package,
370+
"echo",
371+
&args,
372+
server_path,
373+
server_args,
374+
sapphillon_permissions,
375+
);
376+
unsafe {
377+
std::env::remove_var("SAPPHILLON_TEST_SERVER_ABORT");
378+
}
379+
380+
assert!(result.is_err(), "Expected error on abnormal server exit");
381+
assert!(
382+
result
383+
.unwrap_err()
384+
.to_string()
385+
.contains("terminated abnormally"),
386+
"Expected error message to contain 'terminated abnormally'"
387+
);
388+
389+
Ok(())
390+
}
258391
}

0 commit comments

Comments
 (0)