Skip to content

Commit

Permalink
✨ feat: Add IPC between Python and main process!
Browse files Browse the repository at this point in the history
  • Loading branch information
wychlw committed Oct 21, 2024
1 parent 9f3e429 commit 87fb6c7
Show file tree
Hide file tree
Showing 10 changed files with 1,059 additions and 93 deletions.
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ inventory = "0.3.15"
eframe = "0.28.1"
egui_extras = "0.28.1"
termwiz = "0.22.0"
interprocess = "2.2.1"

[toolchain]
channel = "nightly"
Expand Down
85 changes: 75 additions & 10 deletions src/ui/code_editor.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,68 @@
use std::{fs::File, io::Write};
use std::{
fs::File,
io::Write,
sync::mpsc::{channel, Sender},
thread::{spawn, JoinHandle},
};

use eframe::egui::{Context, Id, ScrollArea, TextEdit, TextStyle, Ui, Window};
use egui_extras::syntax_highlighting::{highlight, CodeTheme};
use interprocess::local_socket::Stream;

use crate::{err, impl_sub_window};
use crate::{err, impl_sub_window, info};

use super::{main::SubWindow, pyenv::PyEnv};
use super::{main::SubWindow, pyenv::PyEnv, util::get_main_virt};

enum PyEnyMsg {
Code(String),
End,
}

pub struct CodeEditor {
code: String,
save_to: String,
pyenv: PyEnv,
handler: Option<JoinHandle<()>>,
tx: Option<Sender<PyEnyMsg>>,
}

impl Default for CodeEditor {
fn default() -> Self {
Self {
let mut res = Self {
code: "\
from tester import *
print('Hello, world!')
".to_string(),
s=Shell()
e=ui.UiExec(s, 1)
"
.to_string(),
save_to: "".to_string(),
pyenv: PyEnv::default(),
}
handler: None,
tx: None,
};

let (tx, rx) = channel();

let handler = spawn(move || {
let mut pyenv = PyEnv::build(&get_main_virt());
loop {
match rx.recv() {
Ok(PyEnyMsg::Code(code)) => {
pyenv.run_code(&code);
}
Ok(PyEnyMsg::End) => {
break;
}
Err(e) => {
err!("CodeEditor IPC error: {}", e);
}
}
}
});

res.tx = Some(tx);
res.handler = Some(handler);
res
}
}

Expand All @@ -49,7 +88,15 @@ impl CodeEditor {
});
}
fn run_code(&mut self) {
self.pyenv.run_code(&self.code);
match &self.tx {
Some(tx) => {
let code = self.code.clone();
let _ = tx.send(PyEnyMsg::Code(code));
}
None => {
err!("CodeEditor IPC error: tx is None");
}
}
}
fn save_code(&mut self, ui: &mut Ui) {
if ui.button("Write to file").clicked() {
Expand Down Expand Up @@ -98,13 +145,31 @@ impl CodeEditor {
}
}

impl Drop for CodeEditor {
fn drop(&mut self) {
match &self.tx {
Some(tx) => {
let _ = tx.send(PyEnyMsg::End);
}
None => {}
}
let _ = self.handler.take();
}
}

impl SubWindow for CodeEditor {
fn show(&mut self, ctx: &Context, title: &str, id: &Id, open: &mut bool) {
let window = Window::new(title).id(id.to_owned()).open(open).resizable([true, true]);
let window = Window::new(title)
.id(id.to_owned())
.open(open)
.resizable([true, true]);
window.show(ctx, |ui| {
self.show(ui);
});
}
fn on_ipc(&mut self, msg: &str, _conn: &mut Stream) {
info!("CodeEditor received IPC message: {}", msg);
}
}

impl_sub_window!(CodeEditor, "CodeEditor");
155 changes: 155 additions & 0 deletions src/ui/ipc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//! IPC between windows and sub-windows.
//!
//! As the program may run in multiple processes, or in same process but different address space,
//! we need a way to communicate between windows and sub-windows.
use std::{
error::Error,
io::{BufRead, BufReader, ErrorKind, Write},
};

use interprocess::local_socket::{
prelude::*, traits::ListenerExt, GenericNamespaced, Listener, ListenerNonblockingMode,
ListenerOptions, Stream, ToNsName,
};
use serde::{Deserialize, Serialize};

use crate::{err, log, ui::util::get_sub_virt};

use super::{main::MyApp, util::get_main_virt};

#[derive(Serialize, Deserialize)]
pub struct WindowIpcMessage {
pub window_id: u64,
pub message: String,
}

pub fn init_ipc() -> Result<Listener, Box<dyn Error>> {
let sock_name = get_sock_name(get_main_virt().to_owned(), None);
let sock_name = sock_name.to_ns_name::<GenericNamespaced>()?;

let opts = ListenerOptions::new()
.name(sock_name)
.nonblocking(ListenerNonblockingMode::Both);

let listener = opts.create_sync()?;
Ok(listener)
}

impl MyApp {
pub(super) fn handle_ipc(&mut self) {
for m in self.listener.incoming() {
match m {
Err(e) if e.kind() == ErrorKind::WouldBlock => {
break;
}
Err(e) => {
err!("IPC error: {}", e);
continue;
}
Ok(mut stream) => {
log!("IPC message received");
let mut reader = BufReader::new(&mut stream);
let mut buf = String::new();
let _ = reader.read_line(&mut buf);
log!("Received IPC message: {}", buf);
// let msg: WindowIpcMessage = match from_reader(&mut reader) {
let msg: WindowIpcMessage = match serde_json::from_str(&buf) {
Ok(m) => m,
Err(e) => {
err!("IPC message decode error: {}", e);
continue;
}
};
log!(
"Received IPC message from window {}: {}",
msg.window_id,
msg.message
);
for w in self.sub_windows.iter_mut() {
if w.idx == msg.window_id {
w.window.on_ipc(&msg.message, &mut stream);
}
}
}
}
}
}
}

pub fn get_sock_name(base_name: String, window_id: Option<u64>) -> String {
log!(
"Try get sock name with base name: {}, window id: {:?}",
base_name,
window_id
);
if let Some(id) = window_id {
format!("{}_{}.sock", base_name, id)
} else {
base_name + ".sock"
}
}

pub fn parse_sock_id(sock_name: &str) -> Option<u64> {
log!("Try parse sock id from name: {}", sock_name);
let parts = sock_name.split('_');
let mut id = parts.rev().next()?;
if id.ends_with(".sock") {
id = &id[..id.len() - 5];
}
log!("Parsed sock id: {}", id);
id.parse::<u64>().ok()
}

pub fn sub_send_msg(msg: WindowIpcMessage) {
log!("IPC send message to main window, no response");
let name = get_sock_name(get_sub_virt().to_owned(), None);
log!("Try connected to main window {}", name);
let name = name.to_ns_name::<GenericNamespaced>().unwrap();
let mut conn = Stream::connect(name).unwrap();
log!(
"Send message to main window {}, {}",
msg.window_id,
msg.message
);
serde_json::to_writer(&mut conn, &msg).unwrap();
conn.write(b"\n").unwrap();
}

pub fn sub_send_msg_wait_msg(msg: WindowIpcMessage) -> Result<WindowIpcMessage, Box<dyn Error>> {
log!("IPC send message to main window, with wait for response");
let name = get_sock_name(get_sub_virt().to_owned(), None);
log!("Try connected to main window {}", name);
let name = name.to_ns_name::<GenericNamespaced>()?;
let mut conn = Stream::connect(name)?;
log!(
"Send message to main window {}, {}",
msg.window_id,
msg.message
);
serde_json::to_writer(&mut conn, &msg)?;
conn.write(b"\n")?;
log!("Wait for response from main window");
let mut reader = BufReader::new(&mut conn);
let mut buf = String::new();
reader.read_line(&mut buf)?;
let msg: WindowIpcMessage = serde_json::from_str(&buf)?;
log!(
"Received message from main window {}, {}",
msg.window_id,
msg.message
);
Ok(msg)
}

pub fn main_send_msg(msg: WindowIpcMessage, conn: &mut Stream) {
log!("IPC send message to sub window, no response");
log!(
"Send message to sub window {}, {}",
msg.window_id,
msg.message
);
conn.write(serde_json::to_string(&msg).unwrap().as_bytes())
.unwrap();
conn.write(b"\n").unwrap();
}
Loading

0 comments on commit 87fb6c7

Please sign in to comment.