1# #START_LICENSE###########################################################
2#
3#
4# This file is part of the Environment for Tree Exploration program
5# (ETE).  http://etetoolkit.org
6#
7# ETE is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# ETE is distributed in the hope that it will be useful, but WITHOUT
13# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
15# License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with ETE.  If not, see <http://www.gnu.org/licenses/>.
19#
20#
21#                     ABOUT THE ETE PACKAGE
22#                     =====================
23#
24# ETE is distributed under the GPL copyleft license (2008-2015).
25#
26# If you make use of ETE in published work, please cite:
27#
28# Jaime Huerta-Cepas, Joaquin Dopazo and Toni Gabaldon.
29# ETE: a python Environment for Tree Exploration. Jaime BMC
30# Bioinformatics 2010,:24doi:10.1186/1471-2105-11-24
31#
32# Note that extra references to the specific methods implemented in
33# the toolkit may be available in the documentation.
34#
35# More info at http://etetoolkit.org. Contact: huerta@embl.de
36#
37#
38# #END_LICENSE#############################################################
39from __future__ import absolute_import
40from __future__ import print_function
41
42import sys
43import os
44import re
45import time
46
47from signal import signal, SIGWINCH, SIGKILL, SIGTERM
48from collections import deque
49from textwrap import TextWrapper
50
51import six.moves.queue
52import threading
53
54from .logger import get_main_log
55from .utils import GLOBALS, clear_tempdir, terminate_job_launcher, pjoin, pexist
56from .errors import *
57import six
58from six import StringIO
59
60MAIN_LOG = False
61
62# try:
63#     import curses
64# except ImportError:
65#     NCURSES = False
66# else:
67#     NCURSES = True
68NCURSES = False
69
70# CONVERT shell colors to the same curses palette
71SHELL_COLORS = {
72    "10": '\033[1;37;41m', # white on red
73    "11": '\033[1;37;43m', # white on orange
74    "12": '\033[1;37;45m', # white on magenta
75    "16": '\033[1;37;46m', # white on blue
76    "13": '\033[1;37;40m', # black on white
77    "06": '\033[1;34m', # light blue
78    "05": '\033[1;31m', # light red
79    "03": '\033[1;32m', # light green
80    "8": '\033[1;33m', # yellow
81    "7": '\033[36m', # cyan
82    "6": '\033[34m', # blue
83    "3": '\033[32m', # green
84    "4": '\033[33m', # orange
85    "5": '\033[31m', # red
86    "2": "\033[35m", # magenta
87    "1": "\033[0m", # white
88    "0": "\033[0m", # end
89}
90
91def safe_int(x):
92    try:
93        return int(x)
94    except TypeError:
95        return x
96
97def shell_colorify_match(match):
98    return SHELL_COLORS[match.groups()[2]]
99
100class ExcThread(threading.Thread):
101    def __init__(self, bucket, *args, **kargs):
102        threading.Thread.__init__(self, *args, **kargs)
103        self.bucket = bucket
104
105    def run(self):
106        try:
107            threading.Thread.run(self)
108        except Exception:
109            self.bucket.put(sys.exc_info())
110            raise
111
112class Screen(StringIO):
113    # tags used to control color of strings and select buffer
114    TAG = re.compile("@@((\d+),)?(\d+):", re.MULTILINE)
115    def __init__(self, windows):
116        StringIO.__init__(self)
117        self.windows = windows
118        self.autoscroll = {}
119        self.pos = {}
120        self.lines = {}
121        self.maxsize = {}
122        self.stdout = None
123        self.logfile = None
124        self.wrapper = TextWrapper(width=80, initial_indent="",
125                                   subsequent_indent="         ",
126                                   replace_whitespace=False)
127
128
129        if NCURSES:
130            for windex in windows:
131                h, w = windows[windex][0].getmaxyx()
132                self.maxsize[windex] = (h, w)
133                self.pos[windex] = [0, 0]
134                self.autoscroll[windex] = True
135                self.lines[windex] = 0
136
137    def scroll(self, win, vt, hz=0, refresh=True):
138        line, col = self.pos[win]
139
140        hz_pos = col + hz
141        if hz_pos < 0:
142            hz_pos = 0
143        elif hz_pos >= 1000:
144            hz_pos = 999
145
146        vt_pos = line + vt
147        if vt_pos < 0:
148            vt_pos = 0
149        elif vt_pos >= 1000:
150            vt_pos = 1000 - 1
151
152        if line != vt_pos or col != hz_pos:
153            self.pos[win] = [vt_pos, hz_pos]
154            if refresh:
155                self.refresh()
156
157    def scroll_to(self, win, vt, hz=0, refresh=True):
158        line, col = self.pos[win]
159
160        hz_pos = hz
161        if hz_pos < 0:
162            hz_pos = 0
163        elif hz_pos >= 1000:
164            hz_pos = 999
165
166        vt_pos = vt
167        if vt_pos < 0:
168            vt_pos = 0
169        elif vt_pos >= 1000:
170            vt_pos = 1000 - 1
171
172        if line != vt_pos or col != hz_pos:
173            self.pos[win] = [vt_pos, hz_pos]
174            if refresh:
175                self.refresh()
176
177    def refresh(self):
178        for windex, (win, dim) in six.iteritems(self.windows):
179            h, w, sy, sx = dim
180            line, col = self.pos[windex]
181            if h is not None:
182                win.touchwin()
183                win.noutrefresh(line, col, sy+1, sx+1, sy+h-2, sx+w-2)
184            else:
185                win.noutrefresh()
186        curses.doupdate()
187
188    def write(self, text):
189        if six.PY3:
190            text = str(text)
191        else:
192            if isinstance(text, six.text_type):
193                #text = text.encode(self.stdout.encoding)
194                text = text.encode("UTF-8")
195
196        if NCURSES:
197            self.write_curses(text)
198            if self.logfile:
199                text = re.sub(self.TAG, "", text)
200                self.write_log(text)
201        else:
202            if GLOBALS["color_shell"]:
203                text = re.sub(self.TAG, shell_colorify_match, text)
204            else:
205                text = re.sub(self.TAG, "", text)
206
207            self.write_normal(text)
208            if self.logfile:
209                self.write_log(text)
210
211    def write_log(self, text):
212        self.logfile.write(text)
213        self.logfile.flush()
214
215    def write_normal(self, text):
216        #_text = '\n'.join(self.wrapper.wrap(text))
217        #self.stdout.write(_text+"\n")
218        self.stdout.write(text)
219
220    def write_curses(self, text):
221        formatstr = deque()
222        for m in re.finditer(self.TAG, text):
223            x1, x2  = m.span()
224            cindex = safe_int(m.groups()[2])
225            windex = safe_int(m.groups()[1])
226            formatstr.append([x1, x2, cindex, windex])
227        if not formatstr:
228            formatstr.append([None, 0, 1, 1])
229
230        if formatstr[0][1] == 0:
231            stop, start, cindex, windex = formatstr.popleft()
232            if windex is None:
233                windex = 1
234        else:
235            stop, start, cindex, windex = None, 0, 1, 1
236
237        while start is not None:
238            if formatstr:
239                next_stop, next_start, next_cindex, next_windex = formatstr.popleft()
240            else:
241                next_stop, next_start, next_cindex, next_windex = None, None, cindex, windex
242
243            face = curses.color_pair(cindex)
244            win, (h, w, sy, sx) = self.windows[windex]
245            ln, cn = self.pos[windex]
246            # Is this too inefficient?
247            new_lines = text[start:next_stop].count("\n")
248            self.lines[windex] += new_lines
249            if self.lines[windex] > self.maxsize[windex]:
250                _y, _x = win.getyx()
251
252                for _i in self.lines[windex]-self.maxsize(windex):
253                    win.move(0,0)
254                    win.deleteln()
255                win.move(_y, _x)
256
257            # Visual scroll
258            if self.autoscroll[windex]:
259                scroll = self.lines[windex] - ln - h
260                if scroll > 0:
261                    self.scroll(windex, scroll, refresh=False)
262
263            try:
264                win.addstr(text[start:next_stop], face)
265            except curses.error:
266                win.addstr("???")
267
268            start = next_start
269            stop = next_stop
270            cindex = next_cindex
271            if next_windex is not None:
272                windex = next_windex
273
274        self.refresh()
275
276    def resize_screen(self, s, frame):
277
278        import sys,fcntl,termios,struct
279        data = fcntl.ioctl(self.stdout.fileno(), termios.TIOCGWINSZ, '1234')
280        h, w = struct.unpack('hh', data)
281
282        win = self.windows
283        #main = curses.initscr()
284        #h, w = main.getmaxyx()
285        #win[0] = (main, (None, None, 0, 0))
286        #curses.resizeterm(h, w)
287
288        win[0][0].resize(h, w)
289        win[0][0].clear()
290        info_win, error_win, debug_win = setup_layout(h, w)
291        win[1][1] = info_win
292        win[2][1] = error_win
293        win[3][1] = debug_win
294        self.refresh()
295
296def init_curses(main_scr):
297    if not NCURSES or not main_scr:
298        # curses disabled, no multi windows
299        return None
300
301    # Colors
302    curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
303    curses.init_pair(2, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
304    curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK)
305    curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK)
306    curses.init_pair(5, curses.COLOR_RED, curses.COLOR_BLACK)
307    curses.init_pair(6, curses.COLOR_RED, curses.COLOR_BLACK)
308    curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_RED)
309    curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_YELLOW)
310    curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
311
312    WIN = {}
313    main = main_scr
314    h, w = main.getmaxyx()
315    WIN[0] = (main, (None, None, 0, 0))
316
317    # Creates layout
318    info_win, error_win, debug_win = setup_layout(h, w)
319
320    WIN[1] = [curses.newpad(5000, 1000), info_win]
321    WIN[2] = [curses.newpad(5000, 1000), error_win]
322    WIN[3] = [curses.newpad(5000, 1000), debug_win]
323
324
325    #WIN[1], WIN[11] = newwin(h-1, w/2, 1,1)
326    #WIN[2], WIN[12] = newwin(h-dbg_h-1, (w/2)-1, 1, (w/2)+2)
327    #WIN[3], WIN[13] = newwin(dbg_h-1, (w/2)-1, h-dbg_h+1, (w/2)+2)
328
329    for windex, (w, dim) in six.iteritems(WIN):
330        #w = WIN[i]
331        #w.bkgd(str(windex))
332        w.bkgd(" ")
333        w.keypad(1)
334        w.idlok(True)
335        w.scrollok(True)
336    return WIN
337
338def clear_env():
339    try:
340        terminate_job_launcher()
341    except:
342        pass
343
344    base_dir = GLOBALS["basedir"]
345    lock_file = pjoin(base_dir, "alive")
346    try:
347        os.remove(lock_file)
348    except Exception:
349        print("could not remove lock file %s" %lock_file, file=sys.stderr)
350
351    clear_tempdir()
352
353def app_wrapper(func, args):
354    global NCURSES
355    base_dir = GLOBALS.get("scratch_dir", GLOBALS["basedir"])
356    lock_file = pjoin(base_dir, "alive")
357
358    if not args.enable_ui:
359        NCURSES = False
360
361    if not pexist(lock_file) or args.clearall:
362        open(lock_file, "w").write(time.ctime())
363    else:
364        clear_env()
365        print('\nThe same process seems to be running. Use --clearall or remove the lock file "alive" within the output dir', file=sys.stderr)
366        sys.exit(-1)
367
368    try:
369        if NCURSES:
370            curses.wrapper(main, func, args)
371        else:
372            main(None, func, args)
373    except ConfigError as e:
374        if GLOBALS.get('_background_scheduler', None):
375            GLOBALS['_background_scheduler'].terminate()
376
377        print("\nConfiguration Error:", e, file=sys.stderr)
378        clear_env()
379        sys.exit(-1)
380    except DataError as e:
381        if GLOBALS.get('_background_scheduler', None):
382            GLOBALS['_background_scheduler'].terminate()
383
384        print("\nData Error:", e, file=sys.stderr)
385        clear_env()
386        sys.exit(-1)
387    except KeyboardInterrupt:
388        # Control-C is also grabbed by the back_launcher, so it is no necessary
389        # to terminate from here
390        print("\nProgram was interrupted.", file=sys.stderr)
391        if args.monitor:
392            print(("VERY IMPORTANT !!!: Note that launched"
393                                 " jobs will keep running as you provided the --monitor flag"), file=sys.stderr)
394        clear_env()
395        sys.exit(-1)
396    except:
397        if GLOBALS.get('_background_scheduler', None):
398            GLOBALS['_background_scheduler'].terminate()
399
400        clear_env()
401        raise
402    else:
403        if GLOBALS.get('_background_scheduler', None):
404            GLOBALS['_background_scheduler'].terminate()
405
406        clear_env()
407
408
409def main(main_screen, func, args):
410    """ Init logging and Screen. Then call main function """
411    global MAIN_LOG
412    # Do I use ncurses or basic terminal interface?
413    screen = Screen(init_curses(main_screen))
414
415    # prints are handled by my Screen object
416    screen.stdout = sys.stdout
417    if args.logfile:
418        screen.logfile = open(os.path.join(GLOBALS["basedir"], "etebuild.log"), "w")
419    sys.stdout = screen
420    sys.stderr = screen
421
422    # Start logger, pointing to the selected screen
423    if not MAIN_LOG:
424        MAIN_LOG = True
425        log = get_main_log(screen, [28,26,24,22,20,10][args.verbosity])
426
427    # Call main function as lower thread
428    if NCURSES:
429        screen.refresh()
430        exceptions = six.moves.queue.Queue()
431        t = ExcThread(bucket=exceptions, target=func, args=[args])
432        t.daemon = True
433        t.start()
434        ln = 0
435        chars = "\\|/-\\|/-"
436        cbuff = 1
437        try:
438            while 1:
439                try:
440                    exc = exceptions.get(block=False)
441                except six.moves.queue.Empty:
442                    pass
443                else:
444                    exc_type, exc_obj, exc_trace = exc
445                    # deal with the exception
446                    #print exc_trace, exc_type, exc_obj
447                    raise exc_obj
448
449                mwin = screen.windows[0][0]
450                key = mwin.getch()
451                mwin.addstr(0, 0, "%s (%s) (%s) (%s)" %(key, screen.pos, ["%s %s" %(i,w[1]) for i,w in list(screen.windows.items())], screen.lines) + " "*50)
452                mwin.refresh()
453                if key == 113:
454                    # Fixes the problem of prints without newline char
455                    raise KeyboardInterrupt("Q Pressed")
456                if key == 9:
457                    cbuff += 1
458                    if cbuff>3:
459                        cbuff = 1
460                elif key == curses.KEY_UP:
461                    screen.scroll(cbuff, -1)
462                elif key == curses.KEY_DOWN:
463                    screen.scroll(cbuff, 1)
464                elif key == curses.KEY_LEFT:
465                    screen.scroll(cbuff, 0, -1)
466                elif key == curses.KEY_RIGHT:
467                    screen.scroll(cbuff, 0, 1)
468                elif key == curses.KEY_NPAGE:
469                    screen.scroll(cbuff, 10)
470                elif key == curses.KEY_PPAGE:
471                    screen.scroll(cbuff, -10)
472                elif key == curses.KEY_END:
473                    screen.scroll_to(cbuff, 999, 0)
474                elif key == curses.KEY_HOME:
475                    screen.scroll_to(cbuff, 0, 0)
476                elif key == curses.KEY_RESIZE:
477                    screen.resize_screen(None, None)
478                else:
479                    pass
480        except:
481            # fixes the problem of restoring screen when last print
482            # did not contain a newline char. WTF!
483            print("\n")
484            raise
485
486        #while 1:
487        #    if ln >= len(chars):
488        #        ln = 0
489        #    #screen.windows[0].addstr(0,0, chars[ln])
490        #    #screen.windows[0].refresh()
491        #    time.sleep(0.2)
492        #    ln += 1
493    else:
494        func(args)
495
496def setup_layout(h, w):
497    # Creates layout
498    header = 4
499
500    start_x = 0
501    start_y = header
502    h -= start_y
503    w -= start_x
504
505    h1 = h/2 + h%2
506    h2 = h/2
507    if w > 160:
508        #  _______
509        # |   |___|
510        # |___|___|
511        w1 = w/2 + w%2
512        w2 = w/2
513        info_win = [h, w1, start_y, start_x]
514        error_win = [h1, w2, start_y, w1]
515        debug_win = [h2, w2, h1, w1]
516    else:
517        #  ___
518        # |___|
519        # |___|
520        # |___|
521        h2a = h2/2 + h2%2
522        h2b = h2/2
523        info_win = [h1, w, start_y, start_x]
524        error_win = [h2a, w, h1, start_x]
525        debug_win = [h2b, w, h1+h2a, start_x]
526
527    return info_win, error_win, debug_win
528
529
530