-
Notifications
You must be signed in to change notification settings - Fork 34
/
rdrexecutor.py
284 lines (234 loc) · 9.5 KB
/
rdrexecutor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# ***************************************************************************
# * *
# * Copyright (c) 2022 Howetuft <[email protected]> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2.1 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
"""This module implements a renderer executor object for Render workbench.
The renderer executor allows to run a rendering engine in a responsive,
non-blocking way, provided a command line that should have been generated by a
renderer plugin, and optionnally to display the rendering result (image) in
FreeCAD graphical user interface.
"""
import threading
import shlex
import traceback
from subprocess import Popen, PIPE, STDOUT, SubprocessError
from PySide.QtCore import (
QThread,
Signal,
Slot,
QObject,
QCoreApplication,
QEventLoop,
QMetaObject,
Qt,
)
import FreeCAD as App
from Render.imageviewer import display_image
class RendererWorker(QObject):
"""Worker class to run renderer.
This class embeds the treatment to be executed to run renderer in separate
thread.
"""
finished = Signal(int)
result_ready = Signal(str) # Triggered when result is ready for display
def __init__(self, cmd, img, cwd, open_after_render):
"""Initialize worker.
Args:
cmd -- command to execute (str)
img -- path to resulting image (the renderer output) (str)
cwd -- directory where to execute subprocess
open_after_render -- flag to make GUI open rendered image (bool)
"""
super().__init__()
self.cmd = cmd
self.img = img
self.cwd = cwd
self.open_after_render = open_after_render
# TODO
# if open_after_render:
# self.result_ready.connect(display_image)
def run(self):
"""Run worker.
This method represents the thread activity. It is not intended to be
called directly, but via thread's run() method.
"""
message = App.Console.PrintMessage
warning = App.Console.PrintWarning
error = App.Console.PrintError
result_ready = self.result_ready.emit
message(f"Starting rendering...\n{self.cmd}\n")
try:
# Main loop
with Popen(
shlex.split(self.cmd),
stdout=PIPE,
stderr=STDOUT,
bufsize=1,
universal_newlines=True,
cwd=self.cwd,
) as proc:
for line in proc.stdout:
message(line)
except (OSError, SubprocessError) as err:
errclass = err.__class__.__name__
errmsg = str(err)
error(f"{errclass}: {errmsg}\n")
message("Aborting rendering...\n")
else:
rcode = proc.returncode
msg = f"Exiting rendering - Return code: {rcode}\n"
if not rcode:
message(msg)
else:
warning(msg)
# Open result in GUI if relevant
if self.img:
if App.GuiUp:
result_ready(self.img)
else:
message(f"Output file written to '{self.img}'\n")
# Terminate (for Qt)
self.finished.emit(rcode)
class ExporterWorker(QObject):
"""Worker class to export scene.
This class embeds the treatment to be executed to export scene in separate
thread.
"""
finished = Signal(int)
def __init__(self, func, args, errormsg=None):
"""Initialize worker.
Args:
func -- function to run (callable)
args -- arguments to pass (tuple)
errormsg -- error message
"""
super().__init__()
self.func = func
self.args = args
self.lock = threading.Lock()
self.res = []
self.errormsg = (
errormsg or "[Render][Objstrings] /!\\ EXPORT ERROR /!\\\n"
)
@Slot()
def run(self):
"""Run worker.
This method represents the thread activity. It is not intended to be
called directly, but via thread's run() method.
"""
try:
res = self.func(*self.args)
except Exception as exc: # pylint: disable=broad-exception-caught
App.Console.PrintError(self.errormsg)
traceback.print_exception(exc)
else:
with self.lock:
self.res = res
finally:
# Terminate (for Qt)
self.finished.emit(0)
def result(self):
"""Return result.
Worker must have been joined before, otherwise behaviour is undefined.
"""
with self.lock:
res = self.res
return res
class RendererExecutorGui(QObject):
"""A class to execute a rendering engine in Graphical User Interface mode.
This class is designed to run a worker in a separate thread, keeping GUI
responsive. Meanwhile, stdout/stderr are piped to FreeCAD console, in such
a way it is possible to follow the evolution of the work. To achieve
that, worker is executed in a separate thread, using **QThread**.
Nota: in this class, it is assumed that Qt GUI is up, so it is not tested
anywhere.
"""
def __init__(self, worker):
"""Initialize executor.
Args:
worker -- the worker to run
"""
super().__init__(QCoreApplication.instance())
self.thread = QThread()
self.worker = worker
self.thread.setObjectName("fcd-renderexec")
def start(self):
"""Start executor."""
# Move worker to thread
self.worker.moveToThread(self.thread)
# Connect signals and slots
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.worker.finished.connect(self.thread.exit)
self.thread.finished.connect(self.thread.deleteLater)
if getattr(self.worker, "open_after_render", False):
self.worker.result_ready.connect(self.display_result)
# self.thread.finished.connect(lambda: print("Thread finished")) # Dbg
# Start the thread
self.thread.start()
def join(self):
"""Join thread.
This method is provided for consistency with CLI executor, but it
should not be of much use in GUI context.
"""
loop = QEventLoop()
self.thread.finished.connect(loop.quit, Qt.QueuedConnection)
loop.exec_(flags=QEventLoop.ExcludeUserInputEvents)
@Slot(str)
def display_result(self, img_path):
"""Display result in GUI (slot)."""
# Very important: must execute in main (GUI) thread!
# Therefore, not callable directly from worker
display_image(img_path)
class RendererExecutorCli(threading.Thread):
"""A class to execute a rendering engine in Command Line Interface mode.
This class is designed to run a renderer in a separate thread, keeping CLI
responsive. Meanwhile, stdout/stderr are piped to FreeCAD console, in such
a way it is possible to follow the evolution of the rendering. To achieve
that, renderer is executed in a separate thread, using **Python threads**.
"""
def __init__(self, worker):
"""Initialize executor.
Args:
worker -- the worker to run
"""
super().__init__()
self.worker = worker
def run(self):
"""Run thread.
This method represents the thread activity. It is not intended to be
called directly, but via Thread.start().
"""
self.worker.run()
def exec_in_mainthread(func, *args):
"""Execute a function in the main thread.
Some methods of FreeCAD API require to be executed in main thread.
This function provides a way to do so.
"""
worker = ExporterWorker(func, *args)
main_thread = QCoreApplication.instance().thread()
loop = QEventLoop()
worker.finished.connect(loop.quit)
worker.moveToThread(main_thread)
QMetaObject.invokeMethod(worker, "run", Qt.QueuedConnection)
loop.exec_()
return worker.result()
RendererExecutor = RendererExecutorGui if App.GuiUp else RendererExecutorCli