ActivityTool.py 56.1 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2 3
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

29 30 31 32
import socket
import urllib
import threading
import sys
Vincent Pelletier's avatar
Vincent Pelletier committed
33
from types import StringType
34 35
import re

Jean-Paul Smets's avatar
Jean-Paul Smets committed
36
from Products.CMFCore import CMFCorePermissions
Jean-Paul Smets's avatar
Jean-Paul Smets committed
37
from Products.ERP5Type.Core.Folder import Folder
38
from Products.CMFActivity.ActiveResult import ActiveResult
39
from Products.PythonScripts.Utility import allow_class
40
from AccessControl import ClassSecurityInfo, Permissions
Jérome Perrin's avatar
Jérome Perrin committed
41 42 43 44
from AccessControl.SecurityManagement import newSecurityManager
from AccessControl.SecurityManagement import noSecurityManager
from AccessControl.SecurityManagement import setSecurityManager
from AccessControl.SecurityManagement import getSecurityManager
45 46
from Products.CMFCore.utils import UniqueObject, _getAuthenticatedUser, getToolByName
from Globals import InitializeClass, DTMLFile
Jean-Paul Smets's avatar
Jean-Paul Smets committed
47
from Acquisition import aq_base
48
from Acquisition import aq_inner
49
from ActivityBuffer import ActivityBuffer
50
from zExceptions import ExceptionFormatter
51
from BTrees.OIBTree import OIBTree
52 53 54 55 56 57 58 59 60 61 62 63 64

try:
  from Products import iHotfix
  localizer_lock = iHotfix._the_lock
  localizer_contexts = iHotfix.contexts
  LocalizerContext = iHotfix.Context
except ImportError:
  # Localizer 1.2 includes iHotFix patches
  import Products.Localizer.patches
  localizer_lock = Products.Localizer.patches._requests_lock
  localizer_contexts = Products.Localizer.patches._requests
  LocalizerContext = lambda request: request

65

66
from ZODB.POSException import ConflictError
67
from Products.MailHost.MailHost import MailHostError
Jean-Paul Smets's avatar
Jean-Paul Smets committed
68

69
from zLOG import LOG, INFO, WARNING, ERROR
70
from warnings import warn
71
from time import time
72 73

try:
74
  from Products.TimerService import getTimerService
75
except ImportError:
76 77
  def getTimerService(self):
    pass
Jean-Paul Smets's avatar
Jean-Paul Smets committed
78

79 80 81 82 83
try:
  from traceback import format_list, extract_stack
except ImportError:
  format_list = extract_stack = None

84
# minimal IP:Port regexp
85
NODE_RE = re.compile('^\d+\.\d+\.\d+\.\d+:\d+$')
86

Jean-Paul Smets's avatar
Jean-Paul Smets committed
87 88 89 90
# Using a RAM property (not a property of an instance) allows
# to prevent from storing a state in the ZODB (and allows to restart...)
active_threads = 0
max_active_threads = 1 # 2 will cause more bug to appear (he he)
Vincent Pelletier's avatar
Vincent Pelletier committed
91
is_initialized = False
92 93
tic_lock = threading.Lock() # A RAM based lock to prevent too many concurrent tic() calls
timerservice_lock = threading.Lock() # A RAM based lock to prevent TimerService spamming when busy
94
is_running_lock = threading.Lock()
95
first_run = True
96 97 98
currentNode = None
ROLE_IDLE = 0
ROLE_PROCESSING = 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
99 100 101 102

# Activity Registration
activity_dict = {}

103 104 105 106 107
# Logging channel definitions
import logging
# Main logging channel
activity_logger = logging.getLogger('CMFActivity')
# Some logging subchannels
108
activity_tracking_logger = logging.getLogger('Tracking')
109
activity_timing_logger = logging.getLogger('CMFActivity.TimingLog')
110 111 112 113 114 115 116 117 118 119

# Direct logging to "[instancehome]/log/CMFActivity.log", if this directory exists.
# Otherwise, it will end up in root logging facility (ie, event.log).
from App.config import getConfiguration
import os
instancehome = getConfiguration().instancehome
if instancehome is not None:
  log_directory = os.path.join(instancehome, 'log')
  if os.path.isdir(log_directory):
    from Signals import Signals
120 121
    from ZConfig.components.logger.loghandler import FileHandler
    log_file_handler = FileHandler(os.path.join(log_directory, 'CMFActivity.log'))
122 123 124 125 126 127 128 129
    # Default zope log format string borrowed from
    # ZConfig/components/logger/factory.xml, but without the extra "------"
    # line separating entries.
    log_file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s", "%Y-%m-%dT%H:%M:%S"))
    Signals.registerZopeSignals([log_file_handler])
    activity_logger.addHandler(log_file_handler)
    activity_logger.propagate = 0

130 131 132 133 134 135 136 137
def activity_timing_method(method, args, kw):
  begin = time()
  try:
    return method(*args, **kw)
  finally:
    end = time()
    activity_timing_logger.info('%.02fs: %r(*%r, **%r)' % (end - begin, method, args, kw))

138 139 140 141 142 143 144
# Here go ActivityBuffer instances
# Structure:
#  global_activity_buffer[activity_tool_path][thread_id] = ActivityBuffer
global_activity_buffer = {}
from thread import get_ident, allocate_lock
global_activity_buffer_lock = allocate_lock()

Jean-Paul Smets's avatar
Jean-Paul Smets committed
145 146 147
def registerActivity(activity):
  # Must be rewritten to register
  # class and create instance for each activity
148
  #LOG('Init Activity', 0, str(activity.__name__))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
149 150 151
  activity_instance = activity()
  activity_dict[activity.__name__] = activity_instance

152 153 154 155
MESSAGE_NOT_EXECUTED = 0
MESSAGE_EXECUTED = 1
MESSAGE_NOT_EXECUTABLE = 2

Jean-Paul Smets's avatar
Jean-Paul Smets committed
156
class Message:
157
  """Activity Message Class.
158

159 160
  Message instances are stored in an activity queue, inside the Activity Tool.
  """
161

162
  active_process = None
163 164
  active_process_uid = None

165 166
  def __init__(self, obj, active_process, activity_kw, method_id, args, kw):
    if isinstance(obj, str):
167
      self.object_path = tuple(obj.split('/'))
168
      activity_creation_trace = False
Jean-Paul Smets's avatar
Jean-Paul Smets committed
169
    else:
170
      self.object_path = obj.getPhysicalPath()
171
      activity_creation_trace = obj.getPortalObject().portal_activities.activity_creation_trace
172
    if active_process is not None:
173
      self.active_process = active_process.getPhysicalPath()
174
      self.active_process_uid = active_process.getUid()
175 176 177
    if activity_kw.get('serialization_tag', False) is None:
      # Remove serialization_tag if it's None.
      del activity_kw['serialization_tag']
Jean-Paul Smets's avatar
Jean-Paul Smets committed
178 179 180 181
    self.activity_kw = activity_kw
    self.method_id = method_id
    self.args = args
    self.kw = kw
182
    self.is_executed = MESSAGE_NOT_EXECUTED
Vincent Pelletier's avatar
Vincent Pelletier committed
183 184 185
    self.exc_type = None
    self.exc_value = None
    self.traceback = None
186
    if activity_creation_trace and format_list is not None:
187 188 189 190
      # Save current traceback, to make it possible to tell where a message
      # was generated.
      # Strip last stack entry, since it will always be the same.
      self.call_traceback = ''.join(format_list(extract_stack()[:-1]))
191 192
    else:
      self.call_traceback = None
193
    self.processing = None
194
    self.user_name = str(_getAuthenticatedUser(self))
195
    # Store REQUEST Info
196
    self.request_info = {}
197 198
    request = getattr(obj, 'REQUEST', None)
    if request is not None:
199 200 201 202 203 204 205 206 207
      if 'SERVER_URL' in request.other:
        self.request_info['SERVER_URL'] = request.other['SERVER_URL']
      if 'VirtualRootPhysicalPath' in request.other:
        self.request_info['VirtualRootPhysicalPath'] = \
          request.other['VirtualRootPhysicalPath']
      if 'HTTP_ACCEPT_LANGUAGE' in request.environ:
        self.request_info['HTTP_ACCEPT_LANGUAGE'] = \
          request.environ['HTTP_ACCEPT_LANGUAGE']
      self.request_info['_script'] = list(request._script)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
208

209
  def getObject(self, activity_tool):
210
    """return the object referenced in this message."""
211
    return activity_tool.unrestrictedTraverse(self.object_path)
212

213
  def getObjectList(self, activity_tool):
214
    """return the list of object that can be expanded from this message."""
215
    object_list = []
216
    try:
217
      object_list.append(self.getObject(activity_tool))
218
    except KeyError:
219 220 221 222 223 224
      pass
    else:
      if self.hasExpandMethod():
        expand_method_id = self.activity_kw['expand_method_id']
        # FIXME: how to pass parameters?
        object_list = getattr(object_list[0], expand_method_id)()
225
    return object_list
226

227
  def hasExpandMethod(self):
228 229 230 231 232
    """return true if the message has an expand method.
    An expand method is used to expand the list of objects and to turn a
    big recursive transaction affecting many objects into multiple
    transactions affecting only one object at a time (this can prevent
    duplicated method calls)."""
233
    return self.activity_kw.has_key('expand_method_id')
234

235
  def changeUser(self, user_name, activity_tool):
236
    """restore the security context for the calling user."""
237 238
    uf = activity_tool.getPortalObject().acl_users
    user = uf.getUserById(user_name)
239
    # if the user is not found, try to get it from a parent acl_users
240 241 242 243
    # XXX this is still far from perfect, because we need to store all
    # informations about the user (like original user folder, roles) to
    # replay the activity with exactly the same security context as if
    # it had been executed without activity.
244 245 246
    if user is None:
      uf = activity_tool.getPortalObject().aq_parent.acl_users
      user = uf.getUserById(user_name)
247 248 249
    if user is not None:
      user = user.__of__(uf)
      newSecurityManager(None, user)
250
    else :
251
      LOG("CMFActivity", WARNING,
252
          "Unable to find user %r in the portal" % user_name)
253
      noSecurityManager()
254 255 256 257 258
    return user

  def activateResult(self, activity_tool, result, object):
    if self.active_process is not None:
      active_process = activity_tool.unrestrictedTraverse(self.active_process)
259
      if isinstance(result, ActiveResult):
260 261
        result.edit(object_path=object)
        result.edit(method_id=self.method_id)
262 263
        # XXX Allow other method_id in future
        active_process.activateResult(result)
264
      else:
265
        active_process.activateResult(
266
                    ActiveResult(object_path=object,
267 268
                                 method_id=self.method_id,
                                 result=result)) # XXX Allow other method_id in future
269

Jean-Paul Smets's avatar
Jean-Paul Smets committed
270
  def __call__(self, activity_tool):
271
    try:
272
      obj = self.getObject(activity_tool)
273
    except KeyError:
274
      self.setExecutionState(MESSAGE_NOT_EXECUTABLE, context=activity_tool)
275
    else:
276
      try:
277 278 279 280 281
        old_security_manager = getSecurityManager()
        # Change user if required (TO BE DONE)
        # We will change the user only in order to execute this method
        user = self.changeUser(self.user_name, activity_tool)
        try:
282 283 284
          try:
            # XXX: There is no check to see if user is allowed to access
            # that method !
285 286
            method = getattr(obj, self.method_id)
          except:
287
            method = None
288
            self.setExecutionState(MESSAGE_NOT_EXECUTABLE, context=activity_tool)
289
          else:
290
            if activity_tool.activity_timing_log:
291 292 293
              result = activity_timing_method(method, self.args, self.kw)
            else:
              result = method(*self.args, **self.kw)
294 295 296 297 298
        finally:
          setSecurityManager(old_security_manager)

        if method is not None:
          self.activateResult(activity_tool, result, obj)
299
          self.setExecutionState(MESSAGE_EXECUTED)
300
      except:
301
        self.setExecutionState(MESSAGE_NOT_EXECUTED, context=activity_tool)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
302

303 304 305 306 307 308 309
  def validate(self, activity, activity_tool, check_order_validation=1):
    return activity.validate(activity_tool, self,
                             check_order_validation=check_order_validation,
                             **self.activity_kw)

  def getDependentMessageList(self, activity, activity_tool):
    return activity.getDependentMessageList(activity_tool, self, **self.activity_kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
310

311
  def notifyUser(self, activity_tool, message="Failed Processing Activity"):
312 313
    """Notify the user that the activity failed."""
    portal = activity_tool.getPortalObject()
314
    user_email = portal.getProperty('email_to_address',
315
                       portal.getProperty('email_from_address'))
316

317 318
    email_from_name = portal.getProperty('email_from_name',
                       portal.getProperty('email_from_address'))
319 320 321 322
    call_traceback = ''
    if self.call_traceback:
      call_traceback = 'Created at:\n%s' % self.call_traceback

323
    mail_text = """From: %s <%s>
324 325 326 327 328
To: %s
Subject: %s

%s

329
Node: %s
330
User name: %r
331 332
Document: %s
Method: %s
333 334
Arguments: %r
Named Parameters: %r
335 336
%s

Vincent Pelletier's avatar
Vincent Pelletier committed
337
Exception: %s %s
338

339
%s
340 341
""" % (email_from_name, activity_tool.email_from_address, 
       user_email, message, message,
342
       activity_tool.getCurrentNode(), self.user_name,
343
       '/'.join(self.object_path), self.method_id, self.args, self.kw,
344
       call_traceback, self.exc_type, self.exc_value, self.traceback)
345 346
    try:
      activity_tool.MailHost.send( mail_text )
Vincent Pelletier's avatar
Vincent Pelletier committed
347 348
    except (socket.error, MailHostError), message:
      LOG('ActivityTool.notifyUser', WARNING, 'Mail containing failure information failed to be sent: %s. Exception was: %s %s\n%s' % (message, self.exc_type, self.exc_value, self.traceback))
349

350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
  def reactivate(self, activity_tool):
    # Reactivate the original object.
    obj= self.getObject(activity_tool)
    # Change user if required (TO BE DONE)
    # We will change the user only in order to execute this method
    current_user = str(_getAuthenticatedUser(self))
    user = self.changeUser(self.user_name, activity_tool)
    try:
      active_obj = obj.activate(**self.activity_kw)
      getattr(active_obj, self.method_id)(*self.args, **self.kw)
    finally:
      # Use again the previous user
      if user is not None:
        self.changeUser(current_user, activity_tool)

365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
  def setExecutionState(self, is_executed, exc_info=None, log=True, context=None):
    """
      Set message execution state.

      is_executed can be one of MESSAGE_NOT_EXECUTED, MESSAGE_EXECUTED and
      MESSAGE_NOT_EXECUTABLE (variables defined above).
      
      exc_info must be - if given - similar to sys.exc_info() return value.

      log must be - if given - True or False. If True, a log line will be
      emited with failure details. This parameter should only be used when
      invoking this method on a list of messages to avoid log flood. It is
      caller's responsability to output a log line summing up all errors, and
      to store error in Zope's error_log.

      context must be - if given - an object wrapped in acquisition context.
      It is used to access Zope's error_log object. It is not used if log is
      False.

      If given state is not MESSAGE_EXECUTED, it will also store given
      exc_info. If not given, it will extract one using sys.exc_info().
      If final exc_info does not contain any exception, current stack trace
      will be stored instead: it will hopefuly help understand why message
      is in an error state.
    """
    assert is_executed in (MESSAGE_NOT_EXECUTED, MESSAGE_EXECUTED, MESSAGE_NOT_EXECUTABLE)
    self.is_executed = is_executed
    if is_executed != MESSAGE_EXECUTED:
      if exc_info is None:
        exc_info = sys.exc_info()
395 396 397 398 399 400 401
      if exc_info == (None, None, None):
        # Raise a dummy exception, ignore it, fetch it and use it as if it was the error causing message non-execution. This will help identifyting the cause of this misbehaviour.
        try:
          raise Exception, 'Message execution failed, but there is no exception to explain it. This is a dummy exception so that one can track down why we end up here outside of an exception handling code path.'
        except:
          pass
        exc_info = sys.exc_info()
402 403 404 405 406
      if log:
        LOG('ActivityTool', WARNING, 'Could not call method %s on object %s. Activity created at:\n%s' % (self.method_id, self.object_path, self.call_traceback), error=exc_info)
        # push the error in ZODB error_log
        error_log = getattr(context, 'error_log', None)
        if error_log is not None:
407
          error_log.raising(exc_info)
408 409
      self.exc_type = exc_info[0]
      self.exc_value = str(exc_info[1])
410
      self.traceback = ''.join(ExceptionFormatter.format_exception(*exc_info))
411 412 413 414

  def getExecutionState(self):
    return self.is_executed

Jean-Paul Smets's avatar
Jean-Paul Smets committed
415 416
class Method:

417
  def __init__(self, passive_self, activity, active_process, kw, method_id):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
418 419
    self.__passive_self = passive_self
    self.__activity = activity
420
    self.__active_process = active_process
Jean-Paul Smets's avatar
Jean-Paul Smets committed
421 422 423 424
    self.__kw = kw
    self.__method_id = method_id

  def __call__(self, *args, **kw):
425
    m = Message(self.__passive_self, self.__active_process, self.__kw, self.__method_id, args, kw)
426 427
    portal_activities = self.__passive_self.portal_activities
    if portal_activities.activity_tracking:
428
      activity_tracking_logger.info('queuing message: activity=%s, object_path=%s, method_id=%s, args=%s, kw=%s, activity_kw=%s, user_name=%s' % (self.__activity, '/'.join(m.object_path), m.method_id, m.args, m.kw, m.activity_kw, m.user_name))
429
    activity_dict[self.__activity].queueMessage(portal_activities, m)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
430

431 432
allow_class(Method)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
433 434
class ActiveWrapper:

435
  def __init__(self, passive_self, activity, active_process, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
436 437
    self.__dict__['__passive_self'] = passive_self
    self.__dict__['__activity'] = activity
438
    self.__dict__['__active_process'] = active_process
Jean-Paul Smets's avatar
Jean-Paul Smets committed
439 440 441 442
    self.__dict__['__kw'] = kw

  def __getattr__(self, id):
    return Method(self.__dict__['__passive_self'], self.__dict__['__activity'],
443
                  self.__dict__['__active_process'],
Jean-Paul Smets's avatar
Jean-Paul Smets committed
444 445
                  self.__dict__['__kw'], id)

446 447 448 449
  def __repr__(self):
    return '<%s at 0x%x to %r>' % (self.__class__.__name__, id(self),
                                   self.__dict__['__passive_self'])

450 451 452
# True when activities cannot be executing any more.
has_processed_shutdown = False

453 454 455 456 457 458 459 460
def cancelProcessShutdown():
  """
    This method reverts the effect of calling "process_shutdown" on activity
    tool.
  """
  global has_processed_shutdown
  is_running_lock.release()
  has_processed_shutdown = False
461

462
class ActivityTool (Folder, UniqueObject):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
463
    """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
464 465 466 467 468 469 470 471 472 473 474 475
    ActivityTool is the central point for activity management.

    Improvement to consider to reduce locks:

      Idea 1: create an SQL tool which accumulate queries and executes them at the end of a transaction,
              thus allowing all SQL transaction to happen in a very short time
              (this would also be a great way of using MyISAM tables)

      Idea 2: do the same at the level of ActivityTool

      Idea 3: do the same at the level of each activity (ie. queueMessage
              accumulates and fires messages at the end of the transactino)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
476 477 478
    """
    id = 'portal_activities'
    meta_type = 'CMF Activity Tool'
479
    portal_type = 'Activity Tool'
480
    allowed_types = ( 'CMF Active Process', )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
481 482
    security = ClassSecurityInfo()

483 484
    manage_options = tuple(
                     [ { 'label' : 'Overview', 'action' : 'manage_overview' }
Jean-Paul Smets's avatar
Jean-Paul Smets committed
485
                     , { 'label' : 'Activities', 'action' : 'manageActivities' }
486
                     , { 'label' : 'LoadBalancing', 'action' : 'manageLoadBalancing'}
487
                     , { 'label' : 'Advanced', 'action' : 'manageActivitiesAdvanced' }
Jean-Paul Smets's avatar
Jean-Paul Smets committed
488
                     ,
489
                     ] + list(Folder.manage_options))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
490 491 492 493

    security.declareProtected( CMFCorePermissions.ManagePortal , 'manageActivities' )
    manageActivities = DTMLFile( 'dtml/manageActivities', globals() )

494 495 496
    security.declareProtected( CMFCorePermissions.ManagePortal , 'manageActivitiesAdvanced' )
    manageActivitiesAdvanced = DTMLFile( 'dtml/manageActivitiesAdvanced', globals() )

497 498
    security.declareProtected( CMFCorePermissions.ManagePortal , 'manage_overview' )
    manage_overview = DTMLFile( 'dtml/explainActivityTool', globals() )
499 500 501 502 503 504
    
    security.declareProtected( CMFCorePermissions.ManagePortal , 'manageLoadBalancing' )
    manageLoadBalancing = DTMLFile( 'dtml/manageLoadBalancing', globals() )
    
    distributingNode = ''
    _nodes = ()
505 506 507
    activity_creation_trace = False
    activity_tracking = False
    activity_timing_log = False
508

509 510 511 512 513
    def SQLDict_setPriority(self, **kw):
      real_SQLDict_setPriority = getattr(self.aq_parent, 'SQLDict_setPriority')
      LOG('ActivityTool', 0, real_SQLDict_setPriority(src__=1, **kw))
      return real_SQLDict_setPriority(**kw)

514 515
    def __init__(self):
        return Folder.__init__(self, ActivityTool.id)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
516

517 518 519 520 521 522 523 524 525 526
    # Filter content (ZMI))
    def filtered_meta_types(self, user=None):
        # Filters the list of available meta types.
        all = ActivityTool.inheritedAttribute('filtered_meta_types')(self)
        meta_types = []
        for meta_type in self.all_meta_types():
            if meta_type['name'] in self.allowed_types:
                meta_types.append(meta_type)
        return meta_types

Jean-Paul Smets's avatar
Jean-Paul Smets committed
527 528
    def initialize(self):
      global is_initialized
Sebastien Robin's avatar
Sebastien Robin committed
529
      from Activity import RAMQueue, RAMDict, SQLQueue, SQLDict
Jean-Paul Smets's avatar
Jean-Paul Smets committed
530
      # Initialize each queue
531
      for activity in activity_dict.itervalues():
Jean-Paul Smets's avatar
Jean-Paul Smets committed
532
        activity.initialize(self)
Vincent Pelletier's avatar
Vincent Pelletier committed
533
      is_initialized = True
534 535 536
      
    security.declareProtected(Permissions.manage_properties, 'isSubscribed')
    def isSubscribed(self):
Aurel's avatar
Aurel committed
537
        """
538 539 540 541 542 543 544 545 546 547 548 549
        return True, if we are subscribed to TimerService.
        Otherwise return False.
        """
        service = getTimerService(self)
        if not service:
            LOG('ActivityTool', INFO, 'TimerService not available')
            return False
        
        path = '/'.join(self.getPhysicalPath())
        if path in service.lisSubscriptions():
            return True
        return False
Jean-Paul Smets's avatar
Jean-Paul Smets committed
550

551
    security.declareProtected(Permissions.manage_properties, 'subscribe')
552
    def subscribe(self, REQUEST=None, RESPONSE=None):
553 554
        """ subscribe to the global Timer Service """
        service = getTimerService(self)
555
        url = '%s/manageLoadBalancing?manage_tabs_message=' %self.absolute_url()
556
        if not service:
557
            LOG('ActivityTool', INFO, 'TimerService not available')
558 559 560 561
            url += urllib.quote('TimerService not available')
        else:
            service.subscribe(self)
            url += urllib.quote("Subscribed to Timer Service")
562 563
        if RESPONSE is not None:
            RESPONSE.redirect(url)
564 565

    security.declareProtected(Permissions.manage_properties, 'unsubscribe')
566
    def unsubscribe(self, REQUEST=None, RESPONSE=None):
567 568
        """ unsubscribe from the global Timer Service """
        service = getTimerService(self)
569
        url = '%s/manageLoadBalancing?manage_tabs_message=' %self.absolute_url()
570
        if not service:
571
            LOG('ActivityTool', INFO, 'TimerService not available')
572 573 574 575
            url += urllib.quote('TimerService not available')
        else:
            service.unsubscribe(self)
            url += urllib.quote("Unsubscribed from Timer Service")
576 577
        if RESPONSE is not None:
            RESPONSE.redirect(url)
578

579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656
    security.declareProtected(Permissions.manage_properties, 'isActivityTrackingEnabled')
    def isActivityTrackingEnabled(self):
      return self.activity_tracking

    security.declareProtected(Permissions.manage_properties, 'manage_enableActivityTracking')
    def manage_enableActivityTracking(self, REQUEST=None, RESPONSE=None):
        """
          Enable activity tracing.
        """
        self.activity_tracking = True
        if RESPONSE is not None:
          url = '%s/manageActivitiesAdvanced?manage_tabs_message=' % self.absolute_url()
          url += urllib.quote('Tracking log enabled')
          RESPONSE.redirect(url)

    security.declareProtected(Permissions.manage_properties, 'manage_disableActivityTracking')
    def manage_disableActivityTracking(self, REQUEST=None, RESPONSE=None):
        """
          Disable activity tracing.
        """
        self.activity_tracking = False
        if RESPONSE is not None:
          url = '%s/manageActivitiesAdvanced?manage_tabs_message=' % self.absolute_url()
          url += urllib.quote('Tracking log disabled')
          RESPONSE.redirect(url)

    security.declareProtected(Permissions.manage_properties, 'isActivityTimingLoggingEnabled')
    def isActivityTimingLoggingEnabled(self):
      return self.activity_timing_log

    security.declareProtected(Permissions.manage_properties, 'manage_enableActivityTimingLogging')
    def manage_enableActivityTimingLogging(self, REQUEST=None, RESPONSE=None):
        """
          Enable activity timing logging.
        """
        self.activity_timing_log = True
        if RESPONSE is not None:
          url = '%s/manageActivitiesAdvanced?manage_tabs_message=' % self.absolute_url()
          url += urllib.quote('Timing log enabled')
          RESPONSE.redirect(url)

    security.declareProtected(Permissions.manage_properties, 'manage_disableActivityTimingLogging')
    def manage_disableActivityTimingLogging(self, REQUEST=None, RESPONSE=None):
        """
          Disable activity timing logging.
        """
        self.activity_timing_log = False
        if RESPONSE is not None:
          url = '%s/manageActivitiesAdvanced?manage_tabs_message=' % self.absolute_url()
          url += urllib.quote('Timing log disabled')
          RESPONSE.redirect(url)

    security.declareProtected(Permissions.manage_properties, 'isActivityCreationTraceEnabled')
    def isActivityCreationTraceEnabled(self):
      return self.activity_creation_trace

    security.declareProtected(Permissions.manage_properties, 'manage_enableActivityCreationTrace')
    def manage_enableActivityCreationTrace(self, REQUEST=None, RESPONSE=None):
        """
          Enable activity creation trace.
        """
        self.activity_creation_trace = True
        if RESPONSE is not None:
          url = '%s/manageActivitiesAdvanced?manage_tabs_message=' % self.absolute_url()
          url += urllib.quote('Activity creation trace enabled')
          RESPONSE.redirect(url)

    security.declareProtected(Permissions.manage_properties, 'manage_disableActivityCreationTrace')
    def manage_disableActivityCreationTrace(self, REQUEST=None, RESPONSE=None):
        """
          Disable activity creation trace.
        """
        self.activity_creation_trace = False
        if RESPONSE is not None:
          url = '%s/manageActivitiesAdvanced?manage_tabs_message=' % self.absolute_url()
          url += urllib.quote('Activity creation trace disabled')
          RESPONSE.redirect(url)

657 658
    def manage_beforeDelete(self, item, container):
        self.unsubscribe()
659 660
        Folder.inheritedAttribute('manage_beforeDelete')(self, item, container)
    
661 662
    def manage_afterAdd(self, item, container):
        self.subscribe()
663 664
        Folder.inheritedAttribute('manage_afterAdd')(self, item, container)
       
665 666
    def getCurrentNode(self):
        """ Return current node in form ip:port """
667 668 669 670 671 672 673 674 675 676 677 678 679
        global currentNode
        if currentNode is None:
          port = ''
          from asyncore import socket_map
          for k, v in socket_map.items():
              if hasattr(v, 'port'):
                  # see Zope/lib/python/App/ApplicationManager.py: def getServers(self)
                  type = str(getattr(v, '__class__', 'unknown'))
                  if type == 'ZServer.HTTPServer.zhttp_server':
                      port = v.port
                      break
          ip = socket.gethostbyname(socket.gethostname())
          currentNode = '%s:%s' %(ip, port)
680 681 682 683 684 685 686
        return currentNode
        
    security.declarePublic('getDistributingNode')
    def getDistributingNode(self):
        """ Return the distributingNode """
        return self.distributingNode

687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
    def getNodeList(self, role=None):
      node_dict = self.getNodeDict()
      if role is None:
        result = [x for x in node_dict.keys()]
      else:
        result = [node_id for node_id, node_role in node_dict.items() if node_role == role]
      result.sort()
      return result

    def getNodeDict(self):
      nodes = self._nodes
      if isinstance(nodes, tuple):
        new_nodes = OIBTree()
        new_nodes.update([(x, ROLE_PROCESSING) for x in self._nodes])
        self._nodes = nodes = new_nodes
      return nodes

    def registerNode(self, node):
      node_dict = self.getNodeDict()
      if not node_dict.has_key(node):
        if len(node_dict) == 0: # If we are registering the first node, make
                                # it both the distributing node and a processing
                                # node.
          role = ROLE_PROCESSING
          self.distributingNode = node
        else:
          role = ROLE_IDLE
        self.updateNode(node, role)

    def updateNode(self, node, role):
      node_dict = self.getNodeDict()
      node_dict[node] = role

    security.declareProtected(CMFCorePermissions.ManagePortal, 'getProcessingNodeList')
    def getProcessingNodeList(self):
      return self.getNodeList(role=ROLE_PROCESSING)

724
    security.declareProtected(CMFCorePermissions.ManagePortal, 'getIdleNodeList')
725 726
    def getIdleNodeList(self):
      return self.getNodeList(role=ROLE_IDLE)
727

728 729 730 731
    def _isValidNodeName(self, node_name) :
      """Check we have been provided a good node name"""
      return isinstance(node_name, str) and NODE_RE.match(node_name)
      
732 733
    security.declarePublic('manage_setDistributingNode')
    def manage_setDistributingNode(self, distributingNode, REQUEST=None):
734
        """ set the distributing node """   
735
        if not distributingNode or self._isValidNodeName(distributingNode):
736 737 738 739 740 741 742 743 744 745 746 747 748
          self.distributingNode = distributingNode
          if REQUEST is not None:
              REQUEST.RESPONSE.redirect(
                  REQUEST.URL1 +
                  '/manageLoadBalancing?manage_tabs_message=' +
                  urllib.quote("Distributing Node successfully changed."))
        else :
          if REQUEST is not None:
              REQUEST.RESPONSE.redirect(
                  REQUEST.URL1 +
                  '/manageLoadBalancing?manage_tabs_message=' +
                  urllib.quote("Malformed Distributing Node."))

749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
    security.declareProtected(CMFCorePermissions.ManagePortal, 'manage_delNode')
    def manage_delNode(self, unused_node_list=None, REQUEST=None):
      """ delete selected unused nodes """
      processing_node = self.getDistributingNode()
      updated_processing_node = False
      if unused_node_list is not None:
        node_dict = self.getNodeDict()
        for node in unused_node_list:
          if node in node_dict:
            del node_dict[node]
          if node == processing_node:
            self.processing_node = ''
            updated_processing_node = True
      if REQUEST is not None:
        if unused_node_list is None:
          message = "No unused node selected, nothing deleted."
        else:
          message = "Deleted nodes %r." % (unused_node_list, )
        if updated_processing_node:
          message += "Disabled distributing node because it was deleted."
        REQUEST.RESPONSE.redirect(
          REQUEST.URL1 +
          '/manageLoadBalancing?manage_tabs_message=' +
          urllib.quote(message))

    security.declareProtected(CMFCorePermissions.ManagePortal, 'manage_addToProcessingList')
    def manage_addToProcessingList(self, unused_node_list=None, REQUEST=None):
      """ Change one or more idle nodes into processing nodes """
      if unused_node_list is not None:
        node_dict = self.getNodeDict()
        for node in unused_node_list:
          self.updateNode(node, ROLE_PROCESSING)
      if REQUEST is not None:
        if unused_node_list is None:
          message = "No unused node selected, nothing done."
        else:
          message = "Nodes now procesing: %r." % (unused_node_list, )
        REQUEST.RESPONSE.redirect(
          REQUEST.URL1 +
          '/manageLoadBalancing?manage_tabs_message=' +
          urllib.quote(message))

    security.declareProtected(CMFCorePermissions.ManagePortal, 'manage_removeFromProcessingList')
    def manage_removeFromProcessingList(self, processing_node_list=None, REQUEST=None):
      """ Change one or more procesing nodes into idle nodes """
      if processing_node_list is not None:
        node_dict = self.getNodeDict()
        for node in processing_node_list:
          self.updateNode(node, ROLE_IDLE)
      if REQUEST is not None:
        if processing_node_list is None:
          message = "No used node selected, nothing done."
        else:
          message = "Nodes now unused %r." % (processing_node_list, )
        REQUEST.RESPONSE.redirect(
          REQUEST.URL1 +
          '/manageLoadBalancing?manage_tabs_message=' +
          urllib.quote(message))
807

808 809 810 811 812
    def process_shutdown(self, phase, time_in_phase):
        """
          Prevent shutdown from happening while an activity queue is
          processing a batch.
        """
813
        global has_processed_shutdown
814 815
        if phase == 3 and not has_processed_shutdown:
          has_processed_shutdown = True
816 817 818 819
          LOG('CMFActivity', INFO, "Shutdown: Waiting for activities to finish.")
          is_running_lock.acquire()
          LOG('CMFActivity', INFO, "Shutdown: Activities finished.")

820
    def process_timer(self, tick, interval, prev="", next=""):
821
        """
822 823 824 825 826
        Call distribute() if we are the Distributing Node and call tic()
        with our node number.
        This method is called by TimerService in the interval given
        in zope.conf. The Default is every 5 seconds.
        """
827 828 829 830
        # Prevent TimerService from starting multiple threads in parallel
        acquired = timerservice_lock.acquire(0)
        if not acquired:
          return
831

832
        try:
833
          old_sm = getSecurityManager()
834
          try:
835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866
            try:
              # get owner of portal_catalog, so normally we should be able to
              # have the permission to invoke all activities
              user = self.portal_catalog.getWrappedOwner()
              newSecurityManager(self.REQUEST, user)

              currentNode = self.getCurrentNode()
              self.registerNode(currentNode)
              processing_node_list = self.getNodeList(role=ROLE_PROCESSING)

              # only distribute when we are the distributingNode or if it's empty
              if (self.getDistributingNode() == currentNode):
                self.distribute(len(processing_node_list))

              # SkinsTool uses a REQUEST cache to store skin objects, as
              # with TimerService we have the same REQUEST over multiple
              # portals, we clear this cache to make sure the cache doesn't
              # contains skins from another portal.
              stool = getToolByName(self, 'portal_skins', None)
              if stool is not None:
                stool.changeSkin(None)

              # call tic for the current processing_node
              # the processing_node numbers are the indices of the elements in the node tuple +1
              # because processing_node starts form 1
              if currentNode in processing_node_list:
                self.tic(processing_node_list.index(currentNode)+1)
            except:
              # Catch ALL exception to avoid killing timerserver.
              LOG('ActivityTool', ERROR, 'process_timer received an exception', error=sys.exc_info())
          finally:
            setSecurityManager(old_sm)
Jérome Perrin's avatar
Jérome Perrin committed
867
        finally:
868
          timerservice_lock.release()
869

Jean-Paul Smets's avatar
Jean-Paul Smets committed
870 871 872 873 874 875
    security.declarePublic('distribute')
    def distribute(self, node_count=1):
      """
        Distribute load
      """
      # Initialize if needed
Vincent Pelletier's avatar
Vincent Pelletier committed
876 877
      if not is_initialized:
        self.initialize()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
878 879

      # Call distribute on each queue
880
      for activity in activity_dict.itervalues():
881
        activity.distribute(aq_inner(self), node_count)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
882

Jean-Paul Smets's avatar
Jean-Paul Smets committed
883
    security.declarePublic('tic')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
884
    def tic(self, processing_node=1, force=0):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
885 886
      """
        Starts again an activity
Jean-Paul Smets's avatar
Jean-Paul Smets committed
887
        processing_node starts from 1 (there is not node 0)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
888
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
889
      global active_threads, first_run
Jean-Paul Smets's avatar
Jean-Paul Smets committed
890 891

      # return if the number of threads is too high
892
      # else, increase the number of active_threads and continue
893 894
      tic_lock.acquire()
      too_many_threads = (active_threads >= max_active_threads)
895
      if not too_many_threads or force:
896
        active_threads += 1
897 898 899
      else:
        tic_lock.release()
        raise RuntimeError, 'Too many threads'
900
      tic_lock.release()
901

Jean-Paul Smets's avatar
Jean-Paul Smets committed
902
      # Initialize if needed
Vincent Pelletier's avatar
Vincent Pelletier committed
903 904
      if not is_initialized:
        self.initialize()
905

906
      inner_self = aq_inner(self)
907

908 909 910
      # If this is the first tic after zope is started, reset the processing
      # flag for activities of this node
      if first_run:
911 912 913 914
        inner_self.SQLDict_clearProcessingFlag(
                                processing_node=processing_node)
        inner_self.SQLQueue_clearProcessingFlag(
                                processing_node=processing_node)
915
        first_run = False
916

917
      try:
918 919 920 921 922 923 924
        #Sort activity list by priority
        activity_list = activity_dict.values()
        # Sort method must be local to access "self"
        def cmpActivities(activity_1, activity_2):
          return cmp(activity_1.getPriority(self), activity_2.getPriority(self))
        activity_list.sort(cmpActivities)
        
925
        # Wakeup each queue
926
        for activity in activity_list:
927
          activity.wakeup(inner_self, processing_node)
928

929 930 931 932
        # Process messages on each queue in round robin
        has_awake_activity = 1
        while has_awake_activity:
          has_awake_activity = 0
933
          for activity in activity_list:
934 935 936
            acquired = is_running_lock.acquire(0)
            if acquired:
              try:
937 938
                activity.tic(inner_self, processing_node) # Transaction processing is the responsability of the activity
                has_awake_activity = has_awake_activity or activity.isAwake(inner_self, processing_node)
939 940
              finally:
                is_running_lock.release()
941 942 943 944 945
      finally:
        # decrease the number of active_threads
        tic_lock.acquire()
        active_threads -= 1
        tic_lock.release()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
946

947
    def hasActivity(self, *args, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
948
      # Check in each queue if the object has deferred tasks
949 950
      # if not argument is provided, then check on self
      if len(args) > 0:
951
        obj = args[0]
952
      else:
953
        obj = self
954
      for activity in activity_dict.itervalues():
955
        if activity.hasActivity(aq_inner(self), obj, **kw):
956 957
          return True
      return False
Jean-Paul Smets's avatar
Jean-Paul Smets committed
958

959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974
    def getActivityBuffer(self, create_if_not_found=True):
      """
        Get activtity buffer for this thread for this activity tool.
        If no activity buffer is found at lowest level and create_if_not_found
        is True, create one.
        Intermediate level is unconditionaly created if non existant because
        chances are it will be used in the instance life.
        Lock is held when checking for intermediate level existance
        because:
         - intermediate level dict must not be created in 2 threads at the
           same time, since one creation would destroy the existing one.
        It's released after that step because:
         - lower level access is at thread scope, thus by definition there
           can be only one access at a time to a key
         - GIL protects us when accessing python instances
      """
975 976
      # Safeguard: make sure we are wrapped in  acquisition context before
      # using our path as an activity tool instance-wide identifier.
977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995
      assert getattr(self, 'aq_self', None) is not None
      my_instance_key = self.getPhysicalPath()
      my_thread_key = get_ident()
      global_activity_buffer_lock.acquire()
      try:
        if my_instance_key not in global_activity_buffer:
          global_activity_buffer[my_instance_key] = {}
      finally:
        global_activity_buffer_lock.release()
      thread_activity_buffer = global_activity_buffer[my_instance_key]
      if my_thread_key not in thread_activity_buffer:
        if create_if_not_found:
          buffer = ActivityBuffer(activity_tool=self)
        else:
          buffer = None
        thread_activity_buffer[my_thread_key] = buffer
      activity_buffer = thread_activity_buffer[my_thread_key]
      return activity_buffer

996 997
    security.declarePrivate('activateObject')
    def activateObject(self, object, activity, active_process, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
998 999
      if not is_initialized:
        self.initialize()
1000
      self.getActivityBuffer()
1001
      return ActiveWrapper(object, activity, active_process, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1002

1003
    def deferredQueueMessage(self, activity, message):
1004 1005
      activity_buffer = self.getActivityBuffer()
      activity_buffer.deferredQueueMessage(self, activity, message)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1006

1007
    def deferredDeleteMessage(self, activity, message):
1008 1009
      activity_buffer = self.getActivityBuffer()
      activity_buffer.deferredDeleteMessage(self, activity, message)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1010

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1011
    def getRegisteredMessageList(self, activity):
1012
      activity_buffer = self.getActivityBuffer(create_if_not_found=False)
1013
      if activity_buffer is not None:
1014 1015
        #activity_buffer._register() # This is required if flush flush is called outside activate
        return activity.getRegisteredMessageList(activity_buffer,
1016
                                                 aq_inner(self))
1017 1018
      else:
        return []
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1019

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1020
    def unregisterMessage(self, activity, message):
1021 1022 1023
      activity_buffer = self.getActivityBuffer()
      #activity_buffer._register()
      return activity.unregisterMessage(activity_buffer, aq_inner(self), message)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1024

1025
    def flush(self, obj, invoke=0, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
1026 1027
      if not is_initialized:
        self.initialize()
1028
      self.getActivityBuffer()
1029 1030
      if isinstance(obj, tuple):
        object_path = obj
1031
      else:
1032
        object_path = obj.getPhysicalPath()
1033
      for activity in activity_dict.itervalues():
1034
        activity.flush(aq_inner(self), object_path, invoke=invoke, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1035

1036
    def start(self, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
1037 1038
      if not is_initialized:
        self.initialize()
1039
      for activity in activity_dict.itervalues():
1040
        activity.start(aq_inner(self), **kw)
1041 1042

    def stop(self, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
1043 1044
      if not is_initialized:
        self.initialize()
1045
      for activity in activity_dict.itervalues():
1046
        activity.stop(aq_inner(self), **kw)
1047

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1048
    def invoke(self, message):
1049
      if self.activity_tracking:
1050
        activity_tracking_logger.info('invoking message: object_path=%s, method_id=%s, args=%r, kw=%r, activity_kw=%r, user_name=%s' % ('/'.join(message.object_path), message.method_id, message.args, message.kw, message.activity_kw, message.user_name))
1051
      old_localizer_context = False
1052 1053
      if getattr(self, 'aq_chain', None) is not None:
        # Grab existing acquisition chain and extrach base objects.
1054
        base_chain = [aq_base(x) for x in self.aq_chain]
1055 1056 1057
        # Grab existig request (last chain item) and create a copy.
        request_container = base_chain.pop()
        request = request_container.REQUEST
1058 1059 1060 1061 1062 1063 1064 1065
        # Generate PARENTS value. Sadly, we cannot reuse base_chain since
        # PARENTS items must be wrapped in acquisition
        parents = []
        application = self.getPhysicalRoot().aq_base
        for parent in self.aq_chain:
          if parent.aq_base is application:
            break
          parents.append(parent)
1066 1067
        # XXX: REQUEST.clone() requires PARENTS to be set, and it's not when
        # runing unit tests. Recreate it if it does not exist.
1068 1069
        if getattr(request.other, 'PARENTS', None) is None:
          request.other['PARENTS'] = parents
1070
        # XXX: itools (used by Localizer) requires PATH_INFO to be set, and it's
1071 1072
        # not when runing unit tests. Recreate it if it does not exist.
        if request.environ.get('PATH_INFO') is None:
1073
          request.environ['PATH_INFO'] = '/Control_Panel/timer_service/process_timer'
1074 1075 1076
        
        # restore request information
        new_request = request.clone()
1077
        request_info = message.request_info
1078 1079
        # PARENTS is truncated by clone
        new_request.other['PARENTS'] = parents
1080 1081
        new_request._script = request_info['_script']
        if 'SERVER_URL' in request_info:
1082
          new_request.other['SERVER_URL'] = request_info['SERVER_URL']
1083 1084 1085
        if 'VirtualRootPhysicalPath' in request_info:
          new_request.other['VirtualRootPhysicalPath'] = request_info['VirtualRootPhysicalPath']
        if 'HTTP_ACCEPT_LANGUAGE' in request_info:
1086
          new_request.environ['HTTP_ACCEPT_LANGUAGE'] = request_info['HTTP_ACCEPT_LANGUAGE']
1087 1088
          # Replace Localizer/iHotfix Context, saving existing one
          localizer_context = LocalizerContext(new_request)
1089
          id = get_ident()
1090
          localizer_lock.acquire()
1091
          try:
1092 1093
            old_localizer_context = localizer_contexts.get(id)
            localizer_contexts[id] = localizer_context
1094
          finally:
1095 1096
            localizer_lock.release()
          # Execute Localizer/iHotfix "patch 2"
1097
          new_request.processInputs()
1098 1099

        new_request_container = request_container.__class__(REQUEST=new_request)
1100 1101 1102 1103 1104 1105 1106 1107
        # Recreate acquisition chain.
        my_self = new_request_container
        base_chain.reverse()
        for item in base_chain:
          my_self = item.__of__(my_self)
      else:
        my_self = self
        LOG('CMFActivity.ActivityTool.invoke', INFO, 'Strange: invoke is called outside of acquisition context.')
1108 1109 1110
      try:
        message(my_self)
      finally:
1111 1112 1113 1114
        if my_self is not self: # We rewrapped self
          # Restore default skin selection
          skinnable = self.getPortalObject()
          skinnable.changeSkin(skinnable.getSkinNameFromRequest(request))
1115 1116
        if old_localizer_context is not False:
          # Restore Localizer/iHotfix context
1117
          id = get_ident()
1118
          localizer_lock.acquire()
1119
          try:
1120 1121
            if old_localizer_context is None:
              del localizer_contexts[id]
1122
            else:
1123
              localizer_contexts[id] = old_localizer_context
1124
          finally:
1125
            localizer_lock.release()
1126
      if self.activity_tracking:
1127
        activity_tracking_logger.info('invoked message')
1128 1129 1130
      if my_self is not self: # We rewrapped self
        for held in my_self.REQUEST._held:
          self.REQUEST._hold(held)
1131

1132
    def invokeGroup(self, method_id, message_list):
1133
      if self.activity_tracking:
1134
        activity_tracking_logger.info('invoking group messages: method_id=%s, paths=%s' % (method_id, ['/'.join(m.object_path) for m in message_list]))
1135 1136 1137 1138 1139
      # Invoke a group method.
      object_list = []
      expanded_object_list = []
      new_message_list = []
      path_dict = {}
1140
      # Filter the list of messages. If an object is not available, mark its message as non-executable.
1141 1142
      # In addition, expand an object if necessary, and make sure that no duplication happens.
      for m in message_list:
1143 1144
        # alternate method is used to segregate objects which cannot be grouped.
        alternate_method_id = m.activity_kw.get('alternate_method_id')
1145 1146
        try:
          obj = m.getObject(self)
1147
        except KeyError:
1148
          m.setExecutionState(MESSAGE_NOT_EXECUTABLE, context=self)
1149 1150
          continue
        try:
1151
          i = len(new_message_list) # This is an index of this message in new_message_list.
1152
          if m.hasExpandMethod():
1153 1154
            for subobj in m.getObjectList(self):
              path = subobj.getPath()
1155
              if path not in path_dict:
1156
                path_dict[path] = i
1157 1158 1159 1160 1161 1162
                if alternate_method_id is not None \
                   and hasattr(aq_base(subobj), alternate_method_id):
                  # if this object is alternated, generate a new single active object.
                  activity_kw = m.activity_kw.copy()
                  if 'group_method_id' in activity_kw:
                    del activity_kw['group_method_id']
1163 1164
                  if 'group_id' in activity_kw:
                    del activity_kw['group_id']                    
1165 1166 1167 1168
                  active_obj = subobj.activate(**activity_kw)
                  getattr(active_obj, alternate_method_id)(*m.args, **m.kw)
                else:
                  expanded_object_list.append(subobj)
1169 1170 1171
          else:
            path = obj.getPath()
            if path not in path_dict:
1172
              path_dict[path] = i
1173 1174 1175 1176 1177 1178
              if alternate_method_id is not None \
                  and hasattr(aq_base(obj), alternate_method_id):
                # if this object is alternated, generate a new single active object.
                activity_kw = m.activity_kw.copy()
                if 'group_method_id' in activity_kw:
                  del activity_kw['group_method_id']
1179 1180
                if 'group_id' in activity_kw:
                  del activity_kw['group_id']
1181 1182 1183 1184
                active_obj = obj.activate(**activity_kw)
                getattr(active_obj, alternate_method_id)(*m.args, **m.kw)
              else:
                expanded_object_list.append(obj)
1185
          object_list.append(obj)
1186 1187
          new_message_list.append(m)
        except:
1188
          m.setExecutionState(MESSAGE_NOT_EXECUTED, context=self)
1189

1190 1191
      try:
        if len(expanded_object_list) > 0:
1192 1193
          method = self.unrestrictedTraverse(method_id)
          # FIXME: how to apply security here?
1194 1195
          # NOTE: expanded_object_list must be set to failed objects by the callee.
          #       If it fully succeeds, expanded_object_list must be empty when returning.
1196
          result = method(expanded_object_list, **m.kw)
1197
        else:
1198 1199 1200
          result = None
      except:
        # In this case, the group method completely failed.
1201
        exc_info = sys.exc_info()
1202
        for m in new_message_list:
1203
          m.setExecutionState(MESSAGE_NOT_EXECUTED, exc_info=exc_info, log=False)
1204
        LOG('WARNING ActivityTool', 0,
1205
            'Could not call method %s on objects %s' %
1206
            (method_id, expanded_object_list), error=exc_info)
1207 1208 1209
        error_log = getattr(self, 'error_log', None)
        if error_log is not None:
          error_log.raising(exc_info)
1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222
      else:
        # Obtain all indices of failed messages. Note that this can be a partial failure.
        failed_message_dict = {}
        for obj in expanded_object_list:
          path = obj.getPath()
          i = path_dict[path]
          failed_message_dict[i] = None

        # Only for succeeded messages, an activity process is invoked (if any).
        for i in xrange(len(object_list)):
          object = object_list[i]
          m = new_message_list[i]
          if i in failed_message_dict:
1223
            m.setExecutionState(MESSAGE_NOT_EXECUTED, context=self)
1224 1225 1226 1227
          else:
            try:
              m.activateResult(self, result, object)
            except:
1228
              m.setExecutionState(MESSAGE_NOT_EXECUTED, context=self)
1229
            else:
1230
              m.setExecutionState(MESSAGE_EXECUTED, context=self)
1231
      if self.activity_tracking:
1232
        activity_tracking_logger.info('invoked group messages')
1233

1234 1235
    def newMessage(self, activity, path, active_process,
                   activity_kw, method_id, *args, **kw):
1236
      # Some Security Cheking should be made here XXX
Vincent Pelletier's avatar
Vincent Pelletier committed
1237 1238
      if not is_initialized:
        self.initialize()
1239
      self.getActivityBuffer()
1240
      activity_dict[activity].queueMessage(aq_inner(self),
1241
        Message(path, active_process, activity_kw, method_id, args, kw))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1242

1243
    security.declareProtected( CMFCorePermissions.ManagePortal, 'manageInvoke' )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1244 1245 1246 1247 1248 1249
    def manageInvoke(self, object_path, method_id, REQUEST=None):
      """
        Invokes all methods for object "object_path"
      """
      if type(object_path) is type(''):
        object_path = tuple(object_path.split('/'))
1250
      self.flush(object_path,method_id=method_id,invoke=1)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1251
      if REQUEST is not None:
1252 1253
        return REQUEST.RESPONSE.redirect('%s/%s' %
                (self.absolute_url(), 'manageActivities'))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1254

1255
    security.declareProtected( CMFCorePermissions.ManagePortal, 'manageCancel' )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1256 1257 1258 1259 1260 1261
    def manageCancel(self, object_path, method_id, REQUEST=None):
      """
        Cancel all methods for object "object_path"
      """
      if type(object_path) is type(''):
        object_path = tuple(object_path.split('/'))
1262
      self.flush(object_path,method_id=method_id,invoke=0)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1263
      if REQUEST is not None:
1264 1265
        return REQUEST.RESPONSE.redirect('%s/%s' %
                (self.absolute_url(), 'manageActivities'))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1266

1267 1268
    security.declareProtected( CMFCorePermissions.ManagePortal,
                               'manageClearActivities' )
1269
    def manageClearActivities(self, keep=1, REQUEST=None):
1270 1271 1272 1273 1274
      """
        Clear all activities and recreate tables.
      """
      folder = getToolByName(self, 'portal_skins').activity

1275 1276
      # Obtain all pending messages.
      message_list = []
1277
      if keep:
1278
        for activity in activity_dict.itervalues():
1279 1280 1281 1282 1283 1284
          if hasattr(activity, 'dumpMessageList'):
            try:
              message_list.extend(activity.dumpMessageList(self))
            except ConflictError:
              raise
            except:
1285 1286 1287
              LOG('ActivityTool', WARNING,
                  'could not dump messages from %s' %
                  (activity,), error=sys.exc_info())
1288 1289

      if getattr(folder, 'SQLDict_createMessageTable', None) is not None:
1290 1291 1292 1293 1294
        try:
          folder.SQLDict_dropMessageTable()
        except ConflictError:
          raise
        except:
1295
          LOG('CMFActivity', WARNING,
1296
              'could not drop the message table',
1297 1298 1299
              error=sys.exc_info())
        folder.SQLDict_createMessageTable()

1300
      if getattr(folder, 'SQLQueue_createMessageTable', None) is not None:
1301 1302 1303 1304 1305
        try:
          folder.SQLQueue_dropMessageTable()
        except ConflictError:
          raise
        except:
1306
          LOG('CMFActivity', WARNING,
1307
              'could not drop the message queue table',
1308 1309 1310
              error=sys.exc_info())
        folder.SQLQueue_createMessageTable()

1311 1312 1313
      # Reactivate the messages.
      for m in message_list:
        try:
1314
          m.reactivate(aq_inner(self))
1315 1316 1317 1318
        except ConflictError:
          raise
        except:
          LOG('ActivityTool', WARNING,
1319 1320
              'could not reactivate the message %r, %r' %
              (m.object_path, m.method_id), error=sys.exc_info())
1321

1322
      if REQUEST is not None:
1323 1324 1325 1326 1327 1328
        message = 'Activities%20Cleared'
        if keep:
          message = 'Tables%20Recreated'
        return REQUEST.RESPONSE.redirect(
            '%s/manageActivitiesAdvanced?manage_tabs_message=%s' % (
              self.absolute_url(), message))
1329

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1330
    security.declarePublic('getMessageList')
1331
    def getMessageList(self,**kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1332 1333 1334
      """
        List messages waiting in queues
      """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1335
      # Initialize if needed
Vincent Pelletier's avatar
Vincent Pelletier committed
1336 1337
      if not is_initialized:
        self.initialize()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1338

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1339
      message_list = []
1340
      for activity in activity_dict.itervalues():
Sebastien Robin's avatar
Sebastien Robin committed
1341
        try:
1342
          message_list += activity.getMessageList(aq_inner(self),**kw)
Sebastien Robin's avatar
Sebastien Robin committed
1343 1344
        except AttributeError:
          LOG('getMessageList, could not get message from Activity:',0,activity)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1345 1346
      return message_list

1347 1348 1349 1350 1351 1352
    security.declarePublic('countMessageWithTag')
    def countMessageWithTag(self, value):
      """
        Return the number of messages which match the given tag.
      """
      message_count = 0
1353
      for activity in activity_dict.itervalues():
1354
        message_count += activity.countMessageWithTag(aq_inner(self), value)
Sebastien Robin's avatar
Sebastien Robin committed
1355 1356 1357 1358 1359 1360 1361 1362 1363 1364
      return message_count

    security.declarePublic('countMessage')
    def countMessage(self, **kw):
      """
        Return the number of messages which match the given parameter.

        Parameters allowed:

        method_id : the id of the method
Jérome Perrin's avatar
Jérome Perrin committed
1365
        path : for activities on a particular object
Sebastien Robin's avatar
Sebastien Robin committed
1366 1367 1368 1369
        tag : activities with a particular tag
        message_uid : activities with a particular uid
      """
      message_count = 0
1370
      for activity in activity_dict.itervalues():
1371
        message_count += activity.countMessage(aq_inner(self), **kw)
1372 1373
      return message_count

1374
    security.declareProtected( CMFCorePermissions.ManagePortal , 'newActiveProcess' )
1375
    def newActiveProcess(self, **kw):
1376 1377
      from ActiveProcess import addActiveProcess
      new_id = str(self.generateNewId())
1378
      return addActiveProcess(self, new_id, **kw)
1379

1380
    # Active synchronisation methods
1381
    security.declarePrivate('validateOrder')
1382
    def validateOrder(self, message, validator_id, validation_value):
1383 1384 1385 1386 1387
      message_list = self.getDependentMessageList(message, validator_id, validation_value)
      return len(message_list) > 0

    security.declarePrivate('getDependentMessageList')
    def getDependentMessageList(self, message, validator_id, validation_value):
Vincent Pelletier's avatar
Vincent Pelletier committed
1388 1389
      if not is_initialized:
        self.initialize()
1390
      message_list = []
Vincent Pelletier's avatar
Vincent Pelletier committed
1391
      method_id = "_validate_%s" % validator_id
1392
      for activity in activity_dict.itervalues():
1393 1394 1395 1396 1397 1398
        method = getattr(activity, method_id, None)
        if method is not None:
          result = method(aq_inner(self), message, validation_value)
          if result:
            message_list.extend([(activity, m) for m in result])
      return message_list
1399

Yoshinori Okuji's avatar
Yoshinori Okuji committed
1400 1401
    # Required for tests (time shift)
    def timeShift(self, delay):
Vincent Pelletier's avatar
Vincent Pelletier committed
1402 1403
      if not is_initialized:
        self.initialize()
1404
      for activity in activity_dict.itervalues():
1405
        activity.timeShift(aq_inner(self), delay)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
1406

1407
InitializeClass(ActivityTool)