OS.execute_with_pipe does not create process when return value is assigned to a variable, until application is closed.

Godot Version

Godot 4.3

Question

I’m working on an app, which as one of its main features, can export an AVI file, creating a new process with Movie Maker enabled, when you click a button basically.

I’ve been wanting to make a progress bar on the main process, that shows how many frames out of the total amount it has rendered.
I’ve been using OS.create_process for this, but it only returns a PID, which despite the Godot Docs saying it can be used to monitor processes, it can’t seem to do anything other than kill a process or check if it exists (initially my idea was to take the title of the window which is rendering, since it has current frame information).
But I came across the method OS.execute_with_pipe, and it seemed perfect for me, since I can get its output, among other things. I’m using the same way I was using the OS.create_process method.
However, when I call that method and assign its return value to a variable to get its stdio and PID, the process I’m trying to execute won’t be executed, until I close the main process. When I close it, a new process is created.
When I call the method without getting its return value, it opens the new process pretty much right away.

What could be causing this?

You probably need to use a Thread to run your process without blocking the main thread. Still, you only have access to the stdio and stderr buffers so you won’t be able to access the information you want.

Something like:

extends Node


var thread:Thread


func _ready() -> void:
	var path = "<path to your project>"
	var process = OS.execute_with_pipe("/usr/bin/godot", ["--write-movie", "movie.avi", "--quit-after", "100", "--path", path])
	thread = Thread.new()
	thread.start(read_process.bind(process))


func _process(delta: float) -> void:
	if not thread.is_alive():
		var buffers = thread.wait_to_finish()
		print(buffers["stdio"].get_string_from_utf8())
		print(buffers["stderr"].get_string_from_utf8())
		set_process(false)


func read_process(process:Dictionary) -> Dictionary:
	var buffers:Dictionary = {
		"stdio": PackedByteArray(),
		"stderr": PackedByteArray(),
	}
	var stdio = process.get("stdio", null) as FileAccess
	read(stdio, buffers["stdio"])

	var stderr = process.get("stderr", null) as FileAccess
	read(stderr, buffers["stderr"])

	return buffers


func read(file:FileAccess, buffer:PackedByteArray) -> void:
	if is_instance_valid(file):
			while file.is_open() and file.get_error() == OK:
				buffer.push_back(file.get_8())


1 Like

It’s a shame if I can’t get something like print(current_frame) output from the render process with the main process in real time to have a progress bar… but thanks for the response.

Yea I managed to figure out it has to do with blocking, but even with that in mind and using thread multiple ways, I couldn’t get it to work.

My last and I think closest attempt was something like this:

var global_path: String
var thread: Thread

var p_info: Dictionary

func _process(_delta: float) -> void:
	print(thread.is_alive()) if thread != null else print("smth")

func thread_func() -> void:
	var avi_filename: String = global_path.get_file()
	if ".avi" not in avi_filename:
		avi_filename += ".avi"
	var save_dir: String = global_path.get_base_dir()
	GlobalVariables.save_settings(GlobalVariables.presets_path, true)
	GlobalVariables.save_audio()
	
	var process_info: Dictionary
	process_info = OS.execute_with_pipe(OS.get_executable_path(),
		[EXPORT_SCENE,
		"--path", save_dir,
		"--write-movie", avi_filename,
		"--resolution", "1280x720",
		"--fixed-fps", "60",
		"--",
		"--load_settings=" + GlobalVariables.render_preset_filename
		])
	
	var pipe: FileAccess = process_info["stdio"]
	
	set_deferred("MainUtils.process_info", process_info)
	
	while pipe.is_open() and pipe.get_error() == OK:
		continue

func _on_export_value_pressed() -> void:
	export_file_dialog.popup()

func _on_export_file_dialog_file_selected(path: String) -> void:
	global_path = path
	thread = Thread.new()
	thread.start(thread_func)

Of course, that resulted in blocking. I’d either end up with the main process blocked, or the render one.

A little tired from trying to implement this feature now, but if I give your suggestion a try, I’ll make sure to respond. I appreciate it!

理论上,OS.execute_with_pipe是能立刻得到返回值的。
如果你对它不自信,你可以使用await关键字。
请不要使用while阻塞主进程。
你可试一试直接读取文件
In theory, OS.exe with ipe can immediately obtain the return value.
If you are not confident in it, you can use the await keyword.
Please do not use ‘while’ to block the main process.
You can try reading the file directly

1 Like

Here is how I run a pipe process using ping as an example. But you can use it with any command. One thing to be aware of is if any parameters are being escaped, there could be trouble. The key here is await on signal and thread. In this case I get one line at a time using .get_line(). In yours you may have to use a different get method. Try it an let me know.

extends Control

var pipe
var stderr
var pid
var thread
var info

var bin
var args

# OS.execute_with_pipe()
func _ready(): 
	
	args=["-c10", "yahoo.com"]
	bin = "ping"

	# Initialize pipe
	print("OS.execute_with_pipe:")
	info = OS.execute_with_pipe(bin, args)
	pipe = info["stdio"]
	stderr=info["stderr"]
	pid=info["pid"]
	# start a thread around the pipe
	thread = Thread.new()
	thread.start(_thread_func)
	
	get_window().close_requested.connect(clean_thread)
	
	# wait on thread response from pipe
	var line=""
	while true:
		line = await pipe_in_progress
		if line == null:
			print("Completed!!!!")
			break
		print(line)
	clean_thread()
	
signal pipe_in_progress
func _thread_func():
	var line = ""
	var pipe_err
	while pipe.is_open():
		pipe_err=pipe.get_error() 
		if pipe_err == OK:
			line=pipe.get_line()
			pipe_in_progress.emit.call_deferred(line)
			pass
		else:
			line=stderr.get_line()
			if line!="":
				pipe_in_progress.emit.call_deferred(line)
			else:
				break
	clean_thread()
	
func clean_thread():
	pipe.close()
	OS.kill(pid)