1# -*- coding: utf-8 -*-
2"""@package grass.pygrass.messages
3
4@brief PyGRASS message interface
5
6Fast and exit-safe interface to GRASS C-library message functions
7
8
9(C) 2013 by the GRASS Development Team
10This program is free software under the GNU General Public
11License (>=v2). Read the file COPYING that comes with GRASS
12for details.
13
14@author Soeren Gebbert
15"""
16import sys
17from multiprocessing import Process, Lock, Pipe
18
19import grass.lib.gis as libgis
20
21from grass.exceptions import FatalError
22
23
24def message_server(lock, conn):
25    """The GRASS message server function designed to be a target for
26       multiprocessing.Process
27
28
29       :param lock: A multiprocessing.Lock
30       :param conn: A multiprocessing.Pipe
31
32       This function will use the G_* message C-functions from grass.lib.gis
33       to provide an interface to the GRASS C-library messaging system.
34
35       The data that is send through the pipe must provide an
36       identifier string to specify which C-function should be called.
37
38       The following identifiers are supported:
39
40       - "INFO"       Prints an info message, see G_message() for details
41       - "IMPORTANT"  Prints an important info message,
42                      see G_important_message() for details
43       - "VERBOSE"    Prints a verbose message if the verbosity level is
44                      set accordingly, see G_verbose_message() for details
45       - "WARNING"    Prints a warning message, see G_warning() for details
46       - "ERROR"      Prints a message with a leading "ERROR: " string,
47                      see G_important_message() for details
48       - "PERCENT"    Prints a percent value based on three integer values: n, d and s
49                      see G_percent() for details
50       - "STOP"       Stops the server function and closes the pipe
51       - "FATAL"      Calls G_fatal_error(), this functions is only for
52                      testing purpose
53
54       The that is end through the pipe must be a list of values:
55
56       - Messages: ["INFO|VERBOSE|WARNING|ERROR|FATAL", "MESSAGE"]
57       - Debug:    ["DEBUG", level, "MESSAGE"]
58       - Percent:  ["PERCENT", n, d, s]
59
60    """
61    libgis.G_debug(1, "Start messenger server")
62
63    while True:
64        # Avoid busy waiting
65        conn.poll(None)
66        data = conn.recv()
67        message_type = data[0]
68
69        # Only one process is allowed to write to stderr
70        lock.acquire()
71
72        # Stop the pipe and the infinite loop
73        if message_type == "STOP":
74            conn.close()
75            lock.release()
76            libgis.G_debug(1, "Stop messenger server")
77            sys.exit()
78
79        message = data[1]
80        # libgis limitation
81        if isinstance(message,  type(" ")):
82            if len(message) >= 2000:
83                message = message[:1999]
84
85        if message_type == "PERCENT":
86            n = int(data[1])
87            d = int(data[2])
88            s = int(data[3])
89            libgis.G_percent(n, d, s)
90        elif message_type == "DEBUG":
91            level = data[1]
92            message = data[2]
93            libgis.G_debug(level, message)
94        elif message_type == "VERBOSE":
95            libgis.G_verbose_message(message)
96        elif message_type == "INFO":
97            libgis.G_message(message)
98        elif message_type == "IMPORTANT":
99            libgis.G_important_message(message)
100        elif message_type == "WARNING":
101            libgis.G_warning(message)
102        elif message_type == "ERROR":
103            libgis.G_important_message("ERROR: %s"%message)
104        # This is for testing only
105        elif message_type == "FATAL":
106            libgis.G_fatal_error(message)
107
108        lock.release()
109
110
111class Messenger(object):
112    """Fast and exit-safe interface to GRASS C-library message functions
113
114       This class implements a fast and exit-safe interface to the GRASS
115       C-library message functions like: G_message(), G_warning(),
116       G_important_message(), G_verbose_message(), G_percent() and G_debug().
117
118       Note:
119
120       The C-library message functions a called via ctypes in a subprocess
121       using a pipe (multiprocessing.Pipe) to transfer the text messages.
122       Hence, the process that uses the Messenger interface will not be
123       exited, if a G_fatal_error() was invoked in the subprocess.
124       In this case the Messenger object will simply start a new subprocess
125       and restarts the pipeline.
126
127
128       Usage:
129
130       >>> msgr = Messenger()
131       >>> msgr.debug(0, "debug 0")
132       >>> msgr.verbose("verbose message")
133       >>> msgr.message("message")
134       >>> msgr.important("important message")
135       >>> msgr.percent(1, 1, 1)
136       >>> msgr.warning("Ohh")
137       >>> msgr.error("Ohh no")
138
139       >>> msgr = Messenger()
140       >>> msgr.fatal("Ohh no no no!")
141       Traceback (most recent call last):
142         File "__init__.py", line 239, in fatal
143           sys.exit(1)
144       SystemExit: 1
145
146       >>> msgr = Messenger(raise_on_error=True)
147       >>> msgr.fatal("Ohh no no no!")
148       Traceback (most recent call last):
149         File "__init__.py", line 241, in fatal
150           raise FatalError(message)
151       grass.exceptions.FatalError: Ohh no no no!
152
153       >>> msgr = Messenger(raise_on_error=True)
154       >>> msgr.set_raise_on_error(False)
155       >>> msgr.fatal("Ohh no no no!")
156       Traceback (most recent call last):
157         File "__init__.py", line 239, in fatal
158           sys.exit(1)
159       SystemExit: 1
160
161       >>> msgr = Messenger(raise_on_error=False)
162       >>> msgr.set_raise_on_error(True)
163       >>> msgr.fatal("Ohh no no no!")
164       Traceback (most recent call last):
165         File "__init__.py", line 241, in fatal
166           raise FatalError(message)
167       grass.exceptions.FatalError: Ohh no no no!
168
169    """
170    def __init__(self, raise_on_error=False):
171        self.client_conn = None
172        self.server_conn = None
173        self.server = None
174        self.raise_on_error = raise_on_error
175        self.start_server()
176
177    def start_server(self):
178        """Start the messenger server and open the pipe
179        """
180        self.client_conn, self.server_conn = Pipe()
181        self.lock = Lock()
182        self.server = Process(target=message_server, args=(self.lock,
183                                                           self.server_conn))
184        self.server.daemon = True
185        self.server.start()
186
187    def _check_restart_server(self):
188        """Restart the server if it was terminated
189        """
190        if self.server.is_alive() is True:
191            return
192        self.client_conn.close()
193        self.server_conn.close()
194        self.start_server()
195        self.warning("Needed to restart the messenger server")
196
197    def message(self, message):
198        """Send a message to stderr
199
200        :param message: the text of message
201        :type message: str
202
203           G_message() will be called in the messenger server process
204        """
205        self._check_restart_server()
206        self.client_conn.send(["INFO", message])
207
208    def verbose(self, message):
209        """Send a verbose message to stderr
210
211        :param message: the text of message
212        :type message: str
213
214           G_verbose_message() will be called in the messenger server process
215        """
216        self._check_restart_server()
217        self.client_conn.send(["VERBOSE", message])
218
219    def important(self, message):
220        """Send an important message to stderr
221
222        :param message: the text of message
223        :type message: str
224
225           G_important_message() will be called in the messenger server process
226        """
227        self._check_restart_server()
228        self.client_conn.send(["IMPORTANT", message])
229
230    def warning(self, message):
231        """Send a warning message to stderr
232
233        :param message: the text of message
234        :type message: str
235
236           G_warning() will be called in the messenger server process
237        """
238        self._check_restart_server()
239        self.client_conn.send(["WARNING", message])
240
241    def error(self, message):
242        """Send an error message to stderr
243
244        :param message: the text of message
245        :type message: str
246
247           G_important_message() with an additional "ERROR:" string at
248           the start will be called in the messenger server process
249        """
250        self._check_restart_server()
251        self.client_conn.send(["ERROR", message])
252
253    def fatal(self, message):
254        """Send an error message to stderr, call sys.exit(1) or raise FatalError
255
256        :param message: the text of message
257        :type message: str
258
259           This function emulates the behavior of G_fatal_error(). It prints
260           an error message to stderr and calls sys.exit(1). If raise_on_error
261           is set True while creating the messenger object, a FatalError
262           exception will be raised instead of calling sys.exit(1).
263        """
264        self._check_restart_server()
265        self.client_conn.send(["ERROR", message])
266        self.stop()
267
268        if self.raise_on_error is True:
269            raise FatalError(message)
270        else:
271            sys.exit(1)
272
273    def debug(self, level, message):
274        """Send a debug message to stderr
275
276        :param message: the text of message
277        :type message: str
278
279           G_debug() will be called in the messenger server process
280        """
281        self._check_restart_server()
282        self.client_conn.send(["DEBUG", level, message])
283
284    def percent(self, n, d, s):
285        """Send a percentage to stderr
286
287        :param message: the text of message
288        :type message: str
289
290
291           G_percent() will be called in the messenger server process
292        """
293        self._check_restart_server()
294        self.client_conn.send(["PERCENT", n, d, s])
295
296    def stop(self):
297        """Stop the messenger server and close the pipe
298        """
299        if self.server is not None and self.server.is_alive():
300            self.client_conn.send(["STOP", ])
301            self.server.join(5)
302            self.server.terminate()
303        if self.client_conn is not None:
304            self.client_conn.close()
305
306    def set_raise_on_error(self, raise_on_error=True):
307        """Set the fatal error behavior
308
309           :param raise_on_error: if True a FatalError exception will be
310                                  raised instead of calling sys.exit(1)
311           :type raise_on_error: bool
312
313           - If raise_on_error == True, a FatalError exception will be raised
314             if fatal() is called
315           - If raise_on_error == False, sys.exit(1) will be invoked if
316             fatal() is called
317
318        """
319        self.raise_on_error = raise_on_error
320
321    def get_raise_on_error(self):
322        """Get the fatal error behavior
323
324           :returns: True if a FatalError exception will be raised or False if
325                     sys.exit(1) will be called in case of invoking fatal()
326        """
327        return self.raise_on_error
328
329    def test_fatal_error(self, message):
330        """Force the messenger server to call G_fatal_error()
331        """
332        import time
333        self._check_restart_server()
334        self.client_conn.send(["FATAL", message])
335        time.sleep(1)
336
337
338def get_msgr(_instance=[None, ], *args, **kwargs):
339    """Return a Messenger instance.
340
341       :returns: the Messenger instance.
342
343    >>> msgr0 = get_msgr()
344    >>> msgr1 = get_msgr()
345    >>> msgr2 = Messenger()
346    >>> msgr0 is msgr1
347    True
348    >>> msgr0 is msgr2
349    False
350    """
351    if not _instance[0]:
352        _instance[0] = Messenger(*args, **kwargs)
353    return _instance[0]
354
355
356if __name__ == "__main__":
357    import doctest
358    doctest.testmod()
359