Welcome to mirror list, hosted at ThFree Co, Russian Federation.

subprocess_helper.py « bl_utils « io_blend_utils - git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 024f0da9392c8c64fb8e0f36caca6e0bb743f6fe (plain)
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
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>

"""
Defines an operator mix-in to use for non-blocking command line access.
"""

class SubprocessHelper:
    """
    Mix-in class for operators to run commands in a non-blocking way.

    This uses a modal operator to manage an external process.

    Subclass must define:
        ``command``:
            List of arguments to pass to subprocess.Popen
            report_interval: Time in seconds between updating reports.

        ``process_pre()``:
            Callback that runs before the process executes.

        ``process_post(returncode)``:
            Callback that runs when the process has ende.
            returncode is -1 if the process was terminated.
    """

    @staticmethod
    def _non_blocking_readlines(f, chunk=64):
        """
        Iterate over lines, yielding b'' when nothings left
        or when new data is not yet available.
        """
        import os

        from .pipe_non_blocking import (
                pipe_non_blocking_set,
                pipe_non_blocking_is_error_blocking,
                PortableBlockingIOError,
                )

        fd = f.fileno()
        pipe_non_blocking_set(fd)

        blocks = []

        while True:
            try:
                data = os.read(fd, chunk)
                if not data:
                    # case were reading finishes with no trailing newline
                    yield b''.join(blocks)
                    blocks.clear()
            except PortableBlockingIOError as ex:
                if not pipe_non_blocking_is_error_blocking(ex):
                    raise ex

                yield b''
                continue

            while True:
                n = data.find(b'\n')
                if n == -1:
                    break

                yield b''.join(blocks) + data[:n + 1]
                data = data[n + 1:]
                blocks.clear()
            blocks.append(data)

    def _report_output(self):
        stdout_line_iter, stderr_line_iter = self._buffer_iter
        for line_iter, report_type in (
                (stdout_line_iter, {'INFO'}),
                (stderr_line_iter, {'WARNING'})
                ):
            while True:
                line = next(line_iter).rstrip()  # rstrip all, to include \r on windows
                if not line:
                    break
                self.report(report_type, line.decode(encoding='utf-8', errors='surrogateescape'))

    def _wm_enter(self, context):
        wm = context.window_manager
        window = context.window

        self._timer = wm.event_timer_add(self.report_interval, window)
        window.cursor_set('WAIT')

    def _wm_exit(self, context):
        wm = context.window_manager
        window = context.window

        wm.event_timer_remove(self._timer)
        window.cursor_set('DEFAULT')

    def process_pre(self):
        pass

    def process_post(self, returncode):
        pass

    def modal(self, context, event):
        wm = context.window_manager
        p = self._process

        if event.type == 'ESC':
            self.cancel(context)
            self.report({'INFO'}, "Operation aborted by user")
            return {'CANCELLED'}

        elif event.type == 'TIMER':
            if p.poll() is not None:
                self._report_output()
                self._wm_exit(context)
                self.process_post(p.returncode)
                return {'FINISHED'}

            self._report_output()

        return {'PASS_THROUGH'}

    def execute(self, context):
        import subprocess

        self.process_pre()

        try:
            p = subprocess.Popen(
                    self.command,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    )
        except FileNotFoundError as ex:
            # Command not found
            self.report({'ERROR'}, str(ex))
            return {'CANCELLED'}

        self._process = p
        self._buffer_iter = (
                iter(self._non_blocking_readlines(p.stdout)),
                iter(self._non_blocking_readlines(p.stderr)),
                )

        wm = context.window_manager
        wm.modal_handler_add(self)

        self._wm_enter(context)

        return {'RUNNING_MODAL'}

    def cancel(self, context):
        self._wm_exit(context)
        self._process.kill()
        self.process_post(-1)