最小可重复示例
我创建了一个最小的可复制示例,可以在Github上克隆并轻松运行以进行测试:https://github.com/VirxEC/python-no-run
如果你不想去GitHub,那么代码片段将在这篇文章的底部。
问题,尽快描述
当在Windows上从Rust启动Python时,(Linux运行良好,(从stdin读取会阻止Python子进程启动更多的Python子进程。添加一个快速的sleep(1)
可以让子流程启动,并且运行良好,但如果子流程启动时间超过1秒,那么我就倒霉了。如果它只需要不到1秒的时间,那么我等待的时间就比必须的要长。所以,使用sleep
是我目前实现的快速补丁,但我需要一个合适的解决方案。
问题,详细描述
何时以及为什么
只有当Python仅在Windows上从Rust启动时才会出现此问题,如果您正常运行Python,那么一切都会如预期那样工作。然而,我的应用程序不是Python应用程序,它是一个使用Tauri和Python子组件的Rust GUI,因为有一个大型库,它只是Python,还没有移植到Rust。。。并且由于其大尺寸,可能永远不会被移植。
这也意味着,在生成子流程时,我不能从主Rust流程中获取其他不相关的命令,因为它只是没有读取stdin并查找它们。在我真正的程序中,我开始了一个新的";线程";从线程中读取stdin,因此理论上可以在侧面启动子流程时读取stdin。现在,在生成这些进程时,我决不能让stdin被读取,这只会增加不需要的额外复杂性。
应该发生什么与实际发生什么
问题在于;Hello World"不会因为子流程生成但没有开始执行而被垃圾邮件。我一直在做这件事,我不知道发生了什么。我知道它与stdin有关,但我的程序使用stdin和stdout在主Rust进程和Python进程之间进行通信。
不知怎的,从主线程上的stdin读取会阻碍子进程的启动。我知道这一点,因为如果你用CCD_ 3生成子进程;Hello World"收到垃圾邮件。
此外,如果我在生成子流程后立即添加sleep(1)
,那么一切都正常!耶!但这与理想相去甚远。它就像一个创可贴,对伤口来说不够大,但至少它覆盖了其中的一部分。我已经在我的程序中临时实现了这个解决方案(特别是sleep(3)
,用于生成的每个子流程(,但可能需要生成几十个流程,最终耗时太长。此外,一个过程仍然有可能只需要3秒以上的时间就可以开始,如果真的发生了,我会倒霉的。
borked代码(仅限Windows!(
我删除了导入语句等,因为它们不太相关。如果你感兴趣的话,它们仍然可以在GitHub repo中找到,该repo包含了整个最小的可复制示例。
main.rs
// Setup command
let mut command = Command::new("python");
command.args(["-u", "-c", "from main import start; start()"]);
command.current_dir(std::env::current_dir()?.join("python"));
// Pipe stdin so we can issue commands to tell Python what we want it to do
command.stdin(Stdio::piped());
// Spawn the process & take ownership of stdin
let mut child = command.spawn()?;
let mut stdin = child.stdin.take().unwrap();
// Give a second to make 100% sure Python has started
sleep(Duration::from_secs(1));
// Issue start command to Python, wait 5 seconds then tell it to stop
println!("Writing start");
stdin.write_all(b"start | n")?;
sleep(Duration::from_secs(5));
println!("Writing stop");
stdin.write_all(b"stop | n")?;
// Make sure the process has exited before we exit
child.wait()?;
main.py
def start():
procs = []
while True:
print("Listening for new command...")
command = sys.stdin.readline()
params = command.split(" | ")
print(f"Got command: {command}")
if params[0] == "stop":
print("Stopping...")
# Break from loop
break
elif params[0] == "start":
print("Starting...")
# Spawn the subprocess
proc = subprocess.Popen([sys.executable, "say-hi.py"])
# Add the subprocess to the list so we can kill it later
procs.append(proc)
print("Shutting down")
# Terminate processes
for proc in procs:
proc.terminate()
say-hi.py
while True:
print(f"Hello world!", flush=True)
sleep(1)
我的运行过程
这与GitHub repo中README.md
中的指令类似。
在根文件夹中
- 运行
cargo r
并确保不会发生任何事情 - 在
python/main.py
中,将Popen([sys.executable, "say-hi.py"])
更改为Popen([sys.executable, "say-hi.py"], shell=True)
- 运行CCD_ 14;你好世界"发出停止命令时收到垃圾邮件,并且从未停止
在python
文件夹中
shell
可以是True
或False
,这无关紧要- 运行
python -c "from main import start; start()"
启动进程 - 准确地输入CCD_ 20;你好世界"收到垃圾邮件
- 准确地输入CCD_ 21;你好世界"停止垃圾邮件,成功处理所有退出
subprocess.Popen使用默认参数从windows上的操作系统请求stdout和stdin的句柄。。。它似乎由于某种原因而卡住或失败。
无论如何,传递父级的stdout或PIPE似乎可以在windows上修复它,只需替换
proc = subprocess.Popen([sys.executable, "say-hi.py"])
带有
proc = subprocess.Popen([sys.executable, "say-hi.py"], stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr)
它只是将父级的stdout文件id传递给子级使用。