# -*- coding: iso-8859-1 -*-

# ==============================================================================
# COPYRIGHT (C) 1991 - 2003  EDF R&D                  WWW.CODE-ASTER.ORG
# 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 EDF R&D CODE_ASTER,
#    1 AVENUE DU GENERAL DE GAULLE, 92141 CLAMART CEDEX, FRANCE.
# ==============================================================================

"""Definition of ASTER_RUN class.
"""

__revision__ = "$Id: as_run.py 2686 2006-12-22 16:55:38Z courtois $"
__svntag__ = "$HeadURL: svn://aster.cla.edfgdf.fr/astk/tags/v1.5.0/ASTK_SERV/lib/as_run.py $"

# ----- differ messages translation
def _(mesg):
   return mesg

import sys
import os
import re
import time
import getpass
import traceback
import pprint
from types import StringType, DictType, ListType, TupleType, StringTypes
from optparse import OptionParser, OptionGroup, SUPPRESS_HELP

# ----- import as_run modules
from as_option    import ASTER_OPTION
from utils        import get_absolute_dirname

# ----- version décodée du tag cvs
__version__ = 'VDEV'
# ----- valeurs CVS
_mat = re.search('astk/tags/v([\._0-9rc]+)/ASTK_SERV', __svntag__)
if _mat != None:
   __version__ = _mat.group(1).replace('_', '.')

__copyright__ = 'Copyright (c) 2001-'+time.strftime('%Y')+ \
      ' EDF R&D - http://www.code-aster.org'

class RunAsterError(Exception):
   """Base exception for ASTER_RUN
   """

class ASTER_RUN:
   # Main class  (__doc__ is used as help string for the parser)
   """%(program)s action [options] [arguments]

  Functions :
"""
#-------------------------------------------------------------------------------
   def __init__(self, astk_serv_root):
      """Initialisations. 'astk_serv_root' is the main astk directory containing
      'bin', 'lib' and 'conf' directories ([...]/ASTK/ASTK_SERV).
      """
      self.ExitOnFatalError = True
      self.astk_serv_root = astk_serv_root
      self.aster_root     = get_absolute_dirname(get_absolute_dirname(astk_serv_root))
      self.confdir = os.path.join(astk_serv_root, 'conf')
      self.__initialized = False
      self.__dbg_count = 0
      # ----- verification de la plate-forme
      if os.name != 'posix':
         print _("""This program runs only under Linux/Unix system'
... but its license allows you to port it under another system and propose your
modifications to the author !
Porting should be limited to 'os.system' calls and other
functions of the 'os' module which are not platform independent.
Contact : http://www.code-aster.org""")
         self.Sortie(4)

      # ----- verification de la version de Python
      if sys.hexversion < 0x020300F0:
         print _("This script requires Python 2.3 or higher, sorry !")
         self.Sortie(4)

      # ----- nom du programme
      self.program = 'as_run'
      self.__version__ = __version__
      
      # ----- options, liste des options du parser à stocker dans options
      self.options = { 'verbose': False }
      # always use _add_option to extend options_list
      self.options_list = ['debug', 'verbose', 'force', 'num_job']
      
      # ----- informations about actions : module, method
      # always use set_action to define a new action
      self.actions_info = {}
      # store current action
      self.current_action = ''

      # ----- user preferences and defaults
      self.user_rc = os.path.join(os.getenv('HOME'), '.astkrc', 'config')
      self.user_vars = {
         'editor' :
            {  'val'  : 'nedit',
               'help' : 'editor command' },
         'devel_server_user' :
            {  'val'  : '',
               'help' : 'login on the development server' + os.linesep + \
               '# (name/ip address is usually set in ASTK_SERV/conf/config)' },
         'remote_copy_protocol' :
            {  'val'  : 'RCP',
               'help' : 'remote protocol used to copy files and directories' },
         'remote_shell_protocol' :
            {  'val'  : 'RSH',
               'help' : 'remote protocol used for shell commands' },
      }
      self._add_option(*self.user_vars.keys())

      # ----- formats
      self.fmt = {
         'usage'     : '   - %s :'+os.linesep+'      %s'+os.linesep,
         'verb1'     : ' %-15s = %s',
         'msg+cod'   : os.linesep+'%-18s %s'+os.linesep,
         'msg_info'  : '<INFO>%s %s',
         'silent'    : '      %s %s',
         'title'     : os.linesep+'-'*80+os.linesep+'%s %s'+os.linesep,
         'diag'      : os.linesep+'-'*60+os.linesep+\
                       '--- DIAGNOSTIC JOB : %s'+\
                       os.linesep+'-'*60+os.linesep,
         'exit'      : os.linesep+_('<ERROR> EXIT_CODE=')+'%d'
      }

      # ----- diagnostic
      self.diag = ''

      # ----- list of files/directories to delete on exit
      self.to_delete = []

      # ----- lists of text to print on exit depending on diagnostic
      # this allows to keep important messages when a lot of output is generated
      # and to print them once again at end
      self.print_on_exit = {}

      # ----- do not print timer info on exit by default
      self.timer = None
      self.print_timer = False

      # ----- parser
      p = OptionParser(option_class=ASTER_OPTION,
         usage=self.__doc__ % vars(self),
         version='%prog '+self.__version__+os.linesep+__copyright__)
      self.parser = p
      p.add_option('-v', '--verbose',
         action='store_true', dest='verbose', default=self.options['verbose'],
         help=_('print status messages'))
      p.add_option('-g', '--debug',
         action='store_true', dest='debug', default=False,
         help=_('print debugging information'))
      p.add_option('-f', '--force',
         action='store_true', dest='force', default=False,
         help=_('force operations which can be cached (download, ' \
                'compilation...)'))
      p.add_option('--num_job',
         action='store', dest='num_job', default=str(os.getpid()),
         help=SUPPRESS_HELP)

      # ----- add user's preferences as options
      for k, dV in self.user_vars.items():
         p.add_option('--'+k,
            action='store', dest=k, default=None,
            help=dV['help'].replace(os.linesep, ' ').replace('#', ''))

      # ----- read master configuration file and user preferences
      self.config = {}
      self._read_rc(os.path.join(self.confdir, 'config'), self.config)
      self._init_rc()

      # ----- prepare tmp_user and cache_dir
      # no folder in cache_dir
      tmp_user = os.path.join(self.config['rep_tmp'],
            'astk_'+getpass.getuser())
      self.config['tmp_user'] = tmp_user
      self.config['cache_dir'] = os.path.join(tmp_user, 'cache')
      if not os.path.exists(tmp_user):
         os.makedirs(tmp_user)
      elif not os.access(tmp_user, os.W_OK):
         print _('no write access to %s') % tmp_user
         self.Sortie(4)
      
      if not os.path.exists(self.config['cache_dir']):
         os.mkdir(self.config['cache_dir'])

      # ----- check for 'flasheur'
      flash = os.path.join(os.getenv('HOME'), self.config['flash_srv'])
      self.config['flasheur'] = flash
      if not os.path.exists(flash):
         os.makedirs(flash)
      elif not os.access(flash, os.W_OK):
         print _('no write access to %s') % flash
         self.Sortie(4)

#-------------------------------------------------------------------------------
   def ParseArgs(self):
      """Calls the arguments parser, returns options and arguments.
      """
      opts, args = self.parser.parse_args()
      # ----- all should have been initialized !
      self.__initialized = True
      return opts, args

#-------------------------------------------------------------------------------
   def _init_rc(self):
      """Read user preferences or create the file the first time
      """
      ficrc = self.user_rc
      # ----- check for ressources directory
      if not os.path.isdir(os.path.dirname(ficrc)):
         os.makedirs(os.path.dirname(ficrc))
      
      if not os.path.isfile(ficrc):
         # ----- create ressources file
         fic = open(ficrc, 'w')
         fic.write('# User preferences file' + os.linesep*2)
         fic.write('# You can override here all values of ' \
               'ASTK_SERV/conf/config' + os.linesep*2)
         for var in self.user_vars.keys():
            fic.write(('# %s'+os.linesep+'%s : %s' +os.linesep*2) % \
                  (self.user_vars[var]['help'], var, self.user_vars[var]['val']))
         fic.close()
      # ----- read ressource file
      self._read_rc(ficrc, self.config)

#-------------------------------------------------------------------------------
   def _read_rc(self, ficrc, destdict):
      """Read a ressource file and store variables to 'destdict'.
      """
      if os.path.isfile(ficrc):
         f = open(ficrc, 'r')
         for line in f:
            if not re.search('^[ ]*#', line):
               mat = re.search('([-a-z_A-Z0-9]+) *: *(.*)', line.strip())
               if mat:
                  key   = mat.group(1)
                  value = mat.group(2).strip()
                  # try to convert numbers and boolean values
                  try:
                     if value.isdigit():
                        value = int(value)
                     else:
                        value = float(value)
                  except ValueError:
                     if value in ('True', 'False'):
                        value = eval(value)
                  # repetable ?
                  if destdict.has_key(key) and key in ('vers', 'noeud'):
                     value = str(destdict[key])+' '+str(value)
                  destdict[key] = value
         f.close()
      else:
         print _('file not found : %s') % ficrc
         self.Sortie(4)

#-------------------------------------------------------------------------------
   def _add_action(self, action, info):
      """Set informations about an action
      Prefer use this method over extend manually the dictionnary.
      """
      if not type(action) in StringTypes:
         self.Mess(_('invalid type (expect %s not %s)') % \
               ('string', type(action)), _('<F>_PROGRAM_ERROR'))
      if not type(info) is DictType:
         self.Mess(_('invalid type (expect %s not %s)') % \
               ('dict', type(info)), _('<F>_PROGRAM_ERROR'))
      if action in self.actions_info.keys():
         self.Mess(_('action already defined : %s') % action,
               _('<F>_PROGRAM_ERROR'))
      else:
         self.actions_info[action] = info

#-------------------------------------------------------------------------------
   def _add_option(self, *args):
      """Add one or more option which will be available through getitem.
      Prefer use this method over extend manually the list.
      """
      if not type(args) in (ListType, TupleType):
         self.Mess(_('invalid type (expect %s not %s)') % \
               ('string', type(args)), _('<F>_PROGRAM_ERROR'))
      for opt in args:
         if opt in self.options_list:
            self.Mess(_('option already defined : %s') % opt,
               _('<F>_PROGRAM_ERROR'))
         else:
            self.options_list.append(opt)

#-------------------------------------------------------------------------------
   def SetActions(self, actions_descr, **dico):
      """Add new actions to the parser configuration
         actions_descr  : description of each action :
            key   = action name,
            value = {syntax, help, method}
      Optionnal arguments :
         actions_order  : order to display actions in help (default to '' :
            arbitrary order of dico.keys method)
         group_options  : add a option group to list options (default to True)
         group_title    : title of the option group (default to '')
         actions_group_title : list actions names in the title between
            parenthesis (default to True)
         options_descr  : description of each option (default to {}) :
            key   = option name,
            value = {args, kwargs} passed to add_option parser method
         stored_options : list of options which should be available using
            getitem (default to all "dest" from options_descr)
      """
      # ----- default values
      # actions_order
      if dico.has_key('actions_order'):
         lacts = dico['actions_order']
      else:
         lacts = actions_descr.keys()
         lacts.sort()
      # title
      if dico.has_key('group_title'):
         title = dico['group_title']+' ('
      else:
         title = ''
      # actions_group_title
      if not dico.has_key('actions_group_title'):
         dico['actions_group_title'] = True
      #  group_options
      if not dico.has_key('group_options'):
         dico['group_options'] = True
      # options_descr
      if not dico.has_key('options_descr'):
         dico['options_descr'] = {}
      # stored_options
      if not dico.has_key('stored_options'):
         dico['stored_options'] = [d.values()[1]['dest'] \
               for d in dico['options_descr'].values()]
      # ----- check types
      # dict
      for obj in (actions_descr, dico['options_descr']):
         if not type(obj) is DictType:
            self.Mess(_('invalid type (expect %s not %s)') % \
                  ('dict', type(obj)), _('<F>_PROGRAM_ERROR'))
      # list
      for obj in (lacts, dico['stored_options']):
         if not type(obj) is ListType:
            self.Mess(_('invalid type (expect %s not %s)') % \
                  ('list', type(obj)), _('<F>_PROGRAM_ERROR'))
      prem = True
      for act in lacts:
         descr = actions_descr[act]
         # ----- actions_info
         self._add_action(act, { 'method' : descr['method'] })
         # ----- parser
         if descr['help'] != SUPPRESS_HELP:
            new_usage = self.parser.get_usage()+ \
               (self.fmt['usage'] % \
                  (descr['help'].capitalize(), '%prog --'+act+' '+descr['syntax']))
            self.parser.set_usage(new_usage)
         self.parser.add_option('--'+act,
               action='store_const_once', dest='action', const=act,
               help=SUPPRESS_HELP)
         if not prem:
            title = title+', '
         prem = False
         title = title+('%r' % act)
      title = title+') '
      if not dico['actions_group_title']:
         title = dico['group_title']
      # ----- specific options
      if dico['group_options']:
         group = OptionGroup(self.parser, title=title)
         self.parser.add_option_group(group)
      else:
         group = self.parser
      for opt, descr in dico['options_descr'].items():
         group.add_option(*descr['args'], **descr['kwargs'])
      # ----- add options to the options list to store
      self._add_option(*dico['stored_options'])

#-------------------------------------------------------------------------------
   def PrintConfig(self):
      """Imprime la liste et la valeur des paramètres
      """
      self.parser.print_version()
      print os.linesep+_('Parameters (config attribute)')+' :'
      for key, val in self.config.items():
         print self.fmt['verb1'] % (key, val)
      print os.linesep+_('Options (options attribute)')+' :'
      for key, val in self.options.items():
         print self.fmt['verb1'] % (key, val)

#-------------------------------------------------------------------------------
   def _clear_tmp(self):
      """Empty cache directory if necessary (remove files for which last access
      is older than 'deltat' seconds), and delete all directories
      from 'self.to_delete'.
      """
      ct = time.time()
      deltat = 24*3600
      # ----- config is not set during __init__
      if self.__initialized:
         if self['verbose']:
            print _('Clear cache directory')+' '+self['cache_dir']+'...'
         # ----- avoid deleting such folders : /, /usr or /home !!!
         if re.search('^/.+/.*', self['cache_dir']):
            for root, dirs, files in os.walk(self['cache_dir'], topdown=False):
               for name in files:
                  if ct-os.stat(os.path.join(root, name)).st_atime > deltat:
                     os.remove(os.path.join(root, name))
                  for name in dirs:
                     try:
                        os.rmdir(os.path.join(root, name))
                     except OSError:
                        pass
         # ----- delete each directory in list_dirs
         if self['verbose']:
            print _('Clear temporary files/directories')
         # if a Delete system function has been added
         if hasattr(self, 'Delete'):
            for d in self.to_delete:
               getattr(self, 'Delete')(d)

#-------------------------------------------------------------------------------
   def __getitem__(self, key):
      """Méthode pour accéder facilement aux paramètres de configuration ou à la
      valeur d'une option.
      Les options surchargent les paramètres de configuration.
      """
      if globals().has_key('__'+key+'__'):
         return globals()['__'+key+'__']
      elif self.options.get(key) != None:
         return self.options[key]
      elif self.config.get(key) != None:
         return self.config[key]
      elif self.__initialized:
         self.Mess(_("""'%s' not exist in config or options.
 Perhaps it must be defined in %s.""") % (key, self.user_rc), \
               _('<F>_PROGRAM_ERROR'))

#-------------------------------------------------------------------------------
   def get(self, key, default=None):
      """Semblable à getitem avec la possibilité de retourner une valeur par défaut
      si la valeur n'est pas trouvée.
      """
      if globals().has_key('__'+key+'__'):
         return globals()['__'+key+'__']
      elif self.options.get(key) != None:
         return self.options[key]
      elif self.config.get(key) != None:
         return self.config[key]
      else:
         return default

#-------------------------------------------------------------------------------
   def PostConf(self):
      """Ajuste la configuration dynamiquement (car dépendance au contexte et
      pas seulement au fichier 'config').
      Paramètres déclenchants :
         serv_as_node, only_serv_as_node
      """
      if not self.__initialized or not hasattr(self, 'GetHostName'):
         print _('Warning : ASTER_RUN object not ready for dynamic adjustement !')
         return
      
      # add localhost as a compute node
      if self.config.get('serv_as_node', False):
         l_node = self.config.get('noeud', '').split()
         localhost = self.GetHostName()
         localhost_short = localhost.split('.')[0]
         if not localhost in l_node and not localhost_short in l_node:
            l_node.insert(0, localhost_short)
         if self.config.get('only_serv_as_node', False):
            l_node = [localhost_short,]
         self.config['noeud'] = ' '.join(l_node)

#-------------------------------------------------------------------------------
   def GetGrav(self, cod):
      """Return the severity behind 'cod' as a number to allow comparison
      """
      dgrav = { '?' : -9, 'NO_TEST_RESU' : -1, 'OK' : 0, 'A' : 1, 'NOOK' : 2,
              'S' : 4, 'E' : 6, 'NO_RESU_FILE' : 6, 'F' : 10 }
      g = dgrav['?']
      mat = re.search('<(.)>', cod)
      if cod in dgrav.keys():
         g = dgrav[cod]
      elif re.search('NOOK', cod):
         g = dgrav['NOOK']
      elif mat != None:
         try:
            g = dgrav[mat.group(1)]
         except KeyError:
            pass
      return g

#-------------------------------------------------------------------------------
   def Mess(self, msg, cod='', store=False):
      """Print a message sur stdout,
      'cod' is an error code (format "<.>_...")
         <E> : continue,
         <F> : stop,
         <A> : alarm, continue,
         '' or INFO : for an info,
         SILENT : like an info without <INFO> string,
         TITLE : add a separator.
      If cod='<F>' (without a description), if exists last <E> error is
      transformed into <F>, else <F>_ABNORMAL_ABORT.
      If store is True, 'msg' is stored in print_on_exit dictionnary.
      """
      # ----- gravite
      g0 = self.GetGrav(self.diag)
      g1 = self.GetGrav(cod)
      coderr = cod
      if cod == '<F>':
         if g0 < self.GetGrav('<E>'):
            coderr =  _('<F>_ABNORMAL_ABORT')
         else:
            coderr = self.diag.replace('<E>', '<F>')
      if g1 >= self.GetGrav('<A>'):
         self.DBG('Alarme ou erreur déclenchée :', '%s %s' % (cod, msg),
                  print_traceback=True)

      # ----- message
      if cod == '' or cod == 'INFO':
         fmt = self.fmt['msg_info']
         coderr = ''
      elif cod == 'SILENT':
         fmt = self.fmt['silent']
         coderr = ''
      elif cod == 'TITLE':
         fmt = self.fmt['title']
         coderr = ''
      else:
         fmt = self.fmt['msg+cod']
         # unknown flag
         if g1 == -9:
            coderr = '<I> '+coderr
      print fmt % (coderr, msg)
      sys.stdout.flush()
      # ----- store in print_on_exit
      if store:
         k = '?'
         msg2 = msg
         mat = re.search('<(.)>', cod)
         if mat != None and mat.group(1) in ('A', 'S', 'E', 'F'):
            k = mat.group(1)
            msg2 = self.fmt['msg+cod'] % (coderr, msg)
         elif cod in ('OK', 'NOOK'):
            k = cod
         if not self.print_on_exit.has_key(k):
            self.print_on_exit[k] = [msg2]
         else:
            self.print_on_exit[k].append(msg2)

      # ----- diagnostic le plus défavorable
      if g1 > g0:
         self.diag = coderr
         if g1 == self.GetGrav('<F>'):
            self.Sortie(4)

#-------------------------------------------------------------------------------
   def Sortie(self, exit_code):
      """Exit by printing diagnostic if defined or exit code if not null
      """
      # print important messages
      titles = {
         '?'      : _('Important messages previously printed :')+os.linesep,
         'OK'     : _('Successful messages previously printed:')+os.linesep,
         'NOOK'   : _('NOOK messages previously printed:')+os.linesep,
         'A'      : _('<A> Alarms previously raised :')+os.linesep,
         'S'      : _('<S> errors previously raised :')+os.linesep,
         'E'      : _('<E> errors previously raised :')+os.linesep,
         'F'      : _('<F> errors previously raised :')+os.linesep,
      }
      for k, tit in titles.items():
         lmsg = self.print_on_exit.get(k, [])
         if len(lmsg)>0:
            print self.fmt['title'] % (tit, os.linesep.join(lmsg))
      
      # stop all timers
      if self.print_timer and hasattr(self, 'timer'):
         print os.linesep, self.timer

      if self.diag:
         # change <E> to <F>
         self.diag = self.diag.replace('<E>', '<F>')
         print self.fmt['diag'] % self.diag
      elif exit_code != 0:
         print self.fmt['exit'] % exit_code
      self._clear_tmp()
      # helps to locate error in PROGRAM_ERROR case or in verbose mode
      if self.diag == _('<F>_PROGRAM_ERROR') \
            or not self.ExitOnFatalError \
            or (exit_code != 0 and (self['debug'] or self['verbose'])):
         raise RunAsterError, exit_code
      else:
         sys.exit(exit_code)

#-------------------------------------------------------------------------------
   def CheckOK(self, tole='<S>'):
      """Exit one error more severe than 'tole' occurred.
      """
      if self.GetGrav(self.diag) >= self.GetGrav(tole):
         self.Sortie(4)

#-------------------------------------------------------------------------------
   def ReinitDiag(self, diag='?'):
      """Reinitialize diagnostic (for example if a global diagnostic is stored)
      """
      self.diag = diag

#-------------------------------------------------------------------------------
   def _printDBG(self, *args, **kargs):
      """Print debug information to stderr.
      """
      print_traceback = kargs.get('print_traceback', True)
      file            = kargs.get('file', sys.stderr)
      try:
         self.__dbg_count += 1
         form = """
-------------------------------------------------------------------------------
>>> %(num)09d  %(orig)s

%(content)s
%(traceback)s"""
         formTB = """
Traceback:
%s
"""
         stack = traceback.format_stack(limit=10)
         try:
            ls = []
            for a in args:
               if type(a) in StringTypes:
                  ls.append(a)
               else:
                  ls.append(pprint.pformat(a))
            content = os.linesep.join(ls)
            if len(content) > 800:
               content = content[:800] + os.linesep + '[...]'
         except None:
            content = str(args)
         dinfo = {
            'num'       : self.__dbg_count,
            'orig'      : stack[-3],         # -1 : _printDBG, -2 : DBG ou ASSERT
            'content'   : content,
            'traceback' : '',
         }
         try:
            mat = re.search('File [\'\"]*(.*?)[\'\"]*, *line ([0-9]+), *in (.*)', dinfo['orig'])
            dinfo['orig'] = '[%s@%s:%s]' % (mat.group(3), mat.group(1), mat.group(2))
         except None:
            pass
         if print_traceback:
            dinfo['traceback'] = formTB % (''.join(traceback.format_stack(limit=10)))
         file.write(form % dinfo)
         file.flush()
      except None:
         pass

#-------------------------------------------------------------------------------
   def DBG(self, *args, **kargs):
      """Print debug information to stderr.
      """
      print_traceback = kargs.get('print_traceback', False)
      file            = kargs.get('file', sys.stderr)
      if (not file.isatty()) or self['debug'] or self['verbose']:
         self._printDBG(print_traceback=print_traceback, file=file, *args)

#-------------------------------------------------------------------------------
   def ASSERT(self, condition):
      """Print errors to stdout and stderr.
      """
      if not condition:
         for f in (sys.stdout, sys.stderr):
            self._printDBG('Assertion failed', print_traceback=True, file=f)
