[Erp5-report] r24539 - in /erp5/trunk/products/TIDStorage/bin: TIDStorage.py server.py
nobody at svn.erp5.org
nobody at svn.erp5.org
Mon Nov 10 11:16:51 CET 2008
Author: vincent
Date: Mon Nov 10 11:16:50 2008
New Revision: 24539
URL: http://svn.erp5.org?rev=24539&view=rev
Log:
Give daemon program to a decent name.
Added:
erp5/trunk/products/TIDStorage/bin/TIDStorage.py
- copied unchanged from r24538, erp5/trunk/products/TIDStorage/bin/server.py
Removed:
erp5/trunk/products/TIDStorage/bin/server.py
Removed: erp5/trunk/products/TIDStorage/bin/server.py
URL: http://svn.erp5.org/erp5/trunk/products/TIDStorage/bin/server.py?rev=24538&view=auto
==============================================================================
--- erp5/trunk/products/TIDStorage/bin/server.py [utf8] (original)
+++ erp5/trunk/products/TIDStorage/bin/server.py (removed)
@@ -1,704 +1,0 @@
-#!/usr/bin/python
-
-##############################################################################
-#
-# Copyright (c) 2007, 2008 Nexedi SARL and Contributors. All Rights Reserved.
-# Vincent Pelletier <vincent at nexedi.com>
-#
-# 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.
-#
-##############################################################################
-
-# About errors in TIDStorage logs:
-# - CRITICAL: Decreasing update ignored
-# This error means that any backup started prior to this error can contain
-# incomplete transaction data.
-# This error can happen when TIDStorage did not handle received data in the
-# right order.
-# Example:
-# 3 storages (S1, S2, S3):
-# They all start at TID=1 value.
-# 2 transaction (T1, T2):
-# T1 commits TID 3 on S2, TID 2 on S3
-# T2 commits TID 2 on S1, TID 2 on S2
-# Due to those TIDs, TIDStorage *should* handle data in this order:
-# T2begin, T2commit, T1begin, T1commit
-# Or:
-# T2begin, T1begin, T2commit, T1commit
-# Or even, though it denotes a late handling of T2commit:
-# T2begin, T1begin, T1commit, T2commit
-# But, if TIDStorage handles data in the following order:
-# T1begin, T1commit, T2begin, T2commit
-# *AND* a backup dumps TIDStorage content at a point between T1commit and
-# T2commit, then the backup will contain T2's commit on S2, which has a
-# lower TID than T1's commit on that same storage.
-#
-# - Abort received, but never began
-# and
-# - Commit received, but never began
-# These erros means that packets were lost/never received.
-# This should not happen, since network connection is TCP, and TCP
-# retransmits data.
-# But it happens frequently if TIDStorage is started when Zope is under
-# load. This is because Zope attemped to contact TIDStorage at "begin"
-# step, but could not reach it. Then, at commit (or abort) it could reach
-# TIDStorage, causing the error message.
-# This error is bening, because:
-# - Until bootstrap is complete, no TID is available for backup
-# - When bootstrap is complete, it means that every ZODB got unlocked
-# at some point (since TIDStorage commit happens after ZODB tpc_finish
-# lock release).
-# - When a transaction sends data to multiple ZODBs, there is a point in
-# time when ALL impacted ZODBs are locked.
-# The conclusion of all this is that any transaction started before
-# TIDStorage was available has necesarily finished (commit or abort) at
-# the time bootstrap finished.
-# So no backup can be affected by such message (but backup feasability can
-# get delayed as locks would delay bootstrap end, and hence TID data
-# availability).
-
-import os
-import imp
-import sys
-import pwd
-import grp
-import sets
-import time
-import urllib
-import socket
-import signal
-import getopt
-import SocketServer
-import threading
-import traceback
-from ExchangeProtocol import ClientDisconnected, ExchangeProtocol
-
-class TransactionException(Exception):
- pass
-
-class AlwaysIncreasingDict(dict):
- """
- When inserting/updating a value, check that the new one is strictly
- greater than existing key (or integer 0 value if no value existed for
- given key).
- Values are converted to integers before comparison.
-
- TODO:
- - Do not descend from dict to prevent users from avoiding checks.
- """
- def __init__(self, strict=False, *args, **kw):
- dict.__init__(self, *args, **kw)
- self._strict = strict
-
- def __setitem__(self, key, value):
- if self.get(key, 0) < value:
- dict.__setitem__(self, key, value)
- else:
- if self._strict:
- log('CRITICAL: Decreasing update ignored: key=%r %r <= %r' % \
- (key, value, self.get(key, 0)))
-
- def update(self, other):
- """
- To check for decreases.
- """
- for key, value in other.iteritems():
- self[key] = value
-
-class TransactionTracker:
- """
- Implement transaction tracking.
- This class is not thread-safe.
- A transaction starts with a call to "begin" and ends with a call to
- "finish" with the same identifier.
- "finish" returns payload provided at begin (or True if no payload was
- given) if nothing illegal was detected, otherwise returns False.
-
- Illegal cases:
- - "begin" called twice without intermediate "finish" call
- - "finish" called without a corresponding "begin" call (this includes
- calling "finish" twice)
- """
- def __init__(self):
- self._container = {}
-
- def begin(self, identifier, payload=True):
- if identifier in self._container:
- raise TransactionException, 'Begin called twice in a row.'
- self._container[identifier] = payload
-
- def finish(self, identifier):
- if identifier not in self._container:
- raise TransactionException, 'Finish called without former "begin" call.'
- return self._container.pop(identifier)
-
-class TIDServer(SocketServer.BaseRequestHandler):
- """
- Exchange data with connected peer.
-
- TODO:
- - Implement socket buffering.
- """
- def log(self, message):
- log('%r: %s' % (self.client_address, message))
-
- def dump(self):
- tid_dict = self._tid_storage.dump()
- self._field_exchange.send_dict(tid_dict)
-
- def begin(self):
- identifier = self._field_exchange.recv_field()
- storage_id_list = self._field_exchange.recv_list()
- self._tid_storage.begin(identifier, storage_id_list)
-
- def abort(self):
- identifier = self._field_exchange.recv_field()
- self._tid_storage.abort(identifier)
-
- def commit(self):
- identifier = self._field_exchange.recv_field()
- tid_dict = self._field_exchange.recv_dict()
- self._tid_storage.commit(identifier, tid_dict)
-
- def handle(self):
- global tid_storage
- self._tid_storage = tid_storage
- self._field_exchange = ExchangeProtocol(socket=self.request)
- command_mapping = {
- 'begin': self.begin,
- 'abort': self.abort,
- 'commit': self.commit,
- 'dump': self.dump
- }
- self.log('Connected')
- try:
- # Intercept ClientDisconnected exception to stop thread nicely instead
- # of crashing.
- # Log all others exceptions.
- while True:
- received = self._field_exchange.recv_field()
- command_str = received.lower()
- if command_str == 'quit':
- break
- method = command_mapping.get(command_str)
- if method is not None:
- # Intercept all errors to log it instead of causing disconnection.
- # Except, of course, the ClientDisconnected exception itself.
- try:
- method()
- except ClientDisconnected:
- raise
- except:
- self.log('\n'.join(traceback.format_exception(*sys.exc_info())))
- except ClientDisconnected:
- pass
- except:
- self.log('\n'.join(traceback.format_exception(*sys.exc_info())))
- self.log('Client disconnected')
- self.request.shutdown(socket.SHUT_RDWR)
- self.request.close()
- return
-
-class TIDStorage:
- """
- Store ZODB TIDs for multiple ZODBs.
- Designed to be a singleton.
- Thread-safe.
-
- Consequently, transactions are not bound to a specific connection: If a
- connection is cut after a "begin", reconnecting and issuing "abort" or
- "commit" is valid.
-
- TODO:
- - Use smaller locking areas
- - Improve decision taking algorythm in _unregisterTransactionID (implies
- modifying _registerTransactionIDAndStorageID).
- """
- _storage_id_lock = threading.RLock()
- _next_full_dump = None
- _next_dump = None
- _tid_file = None
- _burst_period = None
- _full_dump_period = None
-
- def __init__(self, tid_file_path=None, burst_period=None, full_dump_period=None):
- self._transaction_tracker = TransactionTracker()
- self._storage = AlwaysIncreasingDict(strict=True)
- self._transcient = AlwaysIncreasingDict()
- self._storage_id_to_transaction_id_list_dict = {}
- self._transaction_id_to_storage_id_list_dict = {}
- self._storage_id_to_storage_id_set_dict = {}
- if tid_file_path is not None:
- self._tid_file = LogFile(tid_file_path)
- self._burst_period = burst_period
- self._full_dump_period = full_dump_period
- now = time.time()
- if full_dump_period is not None:
- self._next_full_dump = now
- if burst_period is not None:
- self._next_dump = now
- self._since_last_burst = sets.Set()
-
- def __repr__(self):
- result = []
- append = result.append
- self._storage_id_lock.acquire()
- try:
- append('_storage_id_to_transaction_id_list_dict=' + \
- repr(self._storage_id_to_transaction_id_list_dict))
- append('_transaction_id_to_storage_id_list_dict=' + \
- repr(self._transaction_id_to_storage_id_list_dict))
- append('_storage_id_to_storage_id_set_dict=' + \
- repr(self._storage_id_to_storage_id_set_dict))
- append('_transcient=' + repr(self._transcient))
- append('_storage=' + repr(self._storage))
- finally:
- self._storage_id_lock.release()
- return '\n'.join(result)
-
- def _registerTransactionIDAndStorageID(self, transaction_id, storage_id_list):
- assert len(storage_id_list) != 0
- assert self._storage_id_lock.acquire(False)
- try:
- # Update transaction_id -> storage_id_list
- assert transaction_id not in self._transaction_id_to_storage_id_list_dict
- self._transaction_id_to_storage_id_list_dict[transaction_id] = storage_id_list
- storage_id_set = sets.Set(storage_id_list)
- storage_id_set_id_set = sets.Set()
- for storage_id in storage_id_list:
- # Update storage_id -> transaction_id_list
- identifier_set = self._storage_id_to_transaction_id_list_dict.get(storage_id)
- if identifier_set is None:
- identifier_set = self._storage_id_to_transaction_id_list_dict[storage_id] = sets.Set()
- assert transaction_id not in identifier_set
- identifier_set.add(transaction_id)
- # Prepare the update storage_id -> storage_id_set
- existing_storage_id_set = self._storage_id_to_storage_id_set_dict.get(storage_id, None)
- if existing_storage_id_set is not None:
- storage_id_set.union_update(existing_storage_id_set)
- storage_id_set_id_set.add(id(existing_storage_id_set))
- # Update storage_id -> storage_id_set
- # Cannot use iteritems because dict is modified in the loop.
- for key, value in self._storage_id_to_storage_id_set_dict.items():
- if id(value) in storage_id_set_id_set:
- self._storage_id_to_storage_id_set_dict[key] = storage_id_set
- for storage_id in storage_id_set:
- self._storage_id_to_storage_id_set_dict[storage_id] = storage_id_set
- finally:
- self._storage_id_lock.release()
-
- def _unregisterTransactionID(self, transaction_id):
- """
- Also transfers from self._transcient to self._storage.
- """
- assert self._storage_id_lock.acquire(False)
- try:
- # Update transaction_id -> storage_id_list and retrieve storage_id_list
- # Raises if not found
- storage_id_list = self._transaction_id_to_storage_id_list_dict.pop(transaction_id)
- # Update storage_id -> transaction_id_list
- for storage_id in storage_id_list:
- identifier_set = self._storage_id_to_transaction_id_list_dict[storage_id]
- # Raises if not found
- identifier_set.remove(transaction_id)
- if len(identifier_set) == 0:
- del self._storage_id_to_transaction_id_list_dict[storage_id]
- # Update storage_id -> storage_id_set
- # Raises if not found
- storage_id_set = self._storage_id_to_storage_id_set_dict[storage_id]
- # Raises if not found
- storage_id_set.remove(storage_id)
- if self._tid_file is not None:
- now = time.time()
- can_full_dump = has_bootstraped and (self._next_full_dump is not None) and (self._next_full_dump < now)
- can_dump = (not can_full_dump) and (self._next_dump is not None) and (self._next_dump < now)
- record_for_dump = can_dump or (self._next_dump is not None)
- append_to_file = has_bootstraped and (can_dump or can_full_dump)
- else:
- append_to_file = record_for_dump = can_dump = can_full_dump = False
- for key, value in self._storage_id_to_storage_id_set_dict.iteritems():
- if len(value) == 0 and key in self._transcient:
- self._storage[key] = self._transcient.pop(key)
- if record_for_dump:
- self._since_last_burst.add(key)
- if append_to_file:
- if can_full_dump:
- to_dump_dict = self._storage
- dump_code = 'f'
- else:
- to_dump_dict = dict([(key, self._storage[key]) for key in self._since_last_burst])
- dump_code = 'd'
- if len(to_dump_dict):
- self._tid_file.write('%.02f %s %r\n' % (now, dump_code, to_dump_dict))
- if can_full_dump:
- self._next_full_dump = now + self._full_dump_period
- if self._next_dump is not None:
- self._next_dump = now + self._burst_period
- self._since_last_burst.clear()
- if not has_bootstraped:
- doBootstrap()
- #if len(self._storage_id_to_transaction_id_list_dict) == 0:
- # self._storage.update(self._transcient)
- # self._transcient.clear()
- finally:
- self._storage_id_lock.release()
-
- def dump(self):
- self._storage_id_lock.acquire()
- try:
- return self._storage.copy()
- finally:
- self._storage_id_lock.release()
-
- def begin(self, transaction_id, storage_id_list):
- self._storage_id_lock.acquire()
- try:
- self._transaction_tracker.begin(transaction_id, storage_id_list)
- self._registerTransactionIDAndStorageID(transaction_id, storage_id_list)
- finally:
- self._storage_id_lock.release()
-
- def abort(self, transaction_id):
- self._storage_id_lock.acquire()
- try:
- try:
- self._transaction_tracker.finish(transaction_id)
- except TransactionException:
- # Overwrite exception message
- raise TransactionException, 'Abort received, but never began'
- self._unregisterTransactionID(transaction_id)
- finally:
- self._storage_id_lock.release()
-
- def commit(self, transaction_id, tid_dict):
- self._storage_id_lock.acquire()
- try:
- try:
- storage_id_list = self._transaction_tracker.finish(transaction_id)
- except TransactionException:
- # Overwrite exception message
- raise TransactionException, 'Commit received, but never began'
- check_dict = tid_dict.copy()
- for storage_id in storage_id_list:
- del check_dict[storage_id]
- assert len(check_dict) == 0
- self._transcient.update(tid_dict)
- self._unregisterTransactionID(transaction_id)
- finally:
- self._storage_id_lock.release()
-
-class BootstrapContent(threading.Thread):
- """
- Thread used to bootstrap TIDStorage content.
- This must be started at first client request, and must be run only once.
- Global boolean "has_bootstraped" is set to true once it succeeded.
- """
-
- def __init__(self, *args, **kw):
- threading.Thread.__init__(self, *args, **kw)
- self.setDaemon(True)
-
- def run(self):
- """
- Contact all zopes to serialize all their storages.
- """
- global has_bootstraped
- base_url = options.base_url
- if base_url is not None:
- log('Bootstrap started')
- storage_id_to_object_path_dict = {}
- for key, value in options.known_tid_storage_identifier_dict.iteritems():
- mountpoint = value[2]
- if mountpoint is None:
- log('Skipping bootstrap of storage %s because its mountpoint is unknown.' % (key, ))
- else:
- storage_id_to_object_path_dict[key] = mountpoint
- target_storage_id_set = sets.ImmutableSet(storage_id_to_object_path_dict.keys())
- known_storage_id_set = sets.ImmutableSet(tid_storage.dump().keys())
- to_check_storage_id_set = target_storage_id_set - known_storage_id_set
- while len(to_check_storage_id_set) and can_bootstrap:
- serialize_url = None
- for storage_id in to_check_storage_id_set:
- if can_bootstrap and storage_id not in tid_storage.dump().keys():
- serialize_url = base_url % (storage_id_to_object_path_dict[storage_id], )
- try:
- # Query a Zope, which will contact this process in return to store
- # the new TID number, making the given storage known.
- page = urllib.urlopen(serialize_url)
- except Exception, message:
- log('Exception during bootstrap (%r):\n%s' % (serialize_url, ''.join(traceback.format_exception(*sys.exc_info()))))
- else:
- log('Opened %r: %r' % (serialize_url, page.read()))
- # Let some time for zope to contact TIDStorage back and fill the gaps.
- time.sleep(5)
- known_storage_id_set = sets.ImmutableSet(tid_storage.dump().keys())
- to_check_storage_id_set = target_storage_id_set - known_storage_id_set
- if len(to_check_storage_id_set):
- log('Bootstrap in progress... Mising storages: %r' % (to_check_storage_id_set, ))
- # Retry a bit later
- time.sleep(60)
- if len(to_check_storage_id_set) == 0:
- log('Bootstrap done (%i storages).' % (len(target_storage_id_set), ))
- has_bootstraped = True
- else:
- log('Bootstrap did not happen because base_url was not given.')
- has_bootstraped = True
-
-bootstrap_content = BootstrapContent()
-has_bootstraped = False
-can_bootstrap = True
-bootstrap_lock = threading.RLock()
-
-def doBootstrap():
- acquired = bootstrap_lock.acquire(False)
- if acquired:
- try:
- if not bootstrap_content.isAlive():
- bootstrap_content.start()
- finally:
- bootstrap_lock.release()
-
-def log(message):
- print >> sys.stdout, '%s: %s' % (time.asctime(), message)
-
-class PoliteThreadingTCPServer(SocketServer.ThreadingTCPServer):
- daemon_threads = True
- allow_reuse_address = True
-
-def main(address, port):
- server = PoliteThreadingTCPServer((address, port), TIDServer)
- try:
- try:
- log('Server listening.')
- server.serve_forever()
- except KeyboardInterrupt:
- log('Shuting down (received KeyboardInterrupt).')
- finally:
- global can_bootstrap
- can_bootstrap = False
- server.server_close()
-
-def openLog():
- return open(options.logfile_name, 'a', 0)
-
-def HUPHandler(signal_number, stack):
- log('Rotating logfile...')
- sys.stdout = sys.stderr = openLog()
- log('Logfile rotated')
-
-def USR1Handler(signal_number, stack):
- log(repr(tid_storage))
-
-def TERMHandler(signal_number, stack):
- log('Received SIGTERM, exiting.')
- raise KeyboardInterrupt, 'Killed by SIGTERM'
-
-def usage():
- print """
-Usage: %(arg0)s [-h] [-n|--nofork|--fg] [-l|--log] [-p|--port] [-a|--address]
- [--pidfile] [--user] [--group] [-s|--status-file] [-b|--burst-period]
- [-F|--full-dump-period] [-c|--config]
-
- -h
- Display this help.
-
- -n
- --nofork
- --fg
- Do not fork in background.
-
- -l filename
- --log filename
- Log to given filename, instead of default %(logfile_name)s.
-
- -p number
- --port number
- Listen to given port number, intead of default %(port)i.
-
- -a address
- --address address
- Listen to interface runing given address, instead of default %(address)s.
-
- --pidfile file_path
- If forking, this file will contain the pid of forked process.
- If this argument is not provided, pid is written to %(pidfile_name)s.
-
- --user user_name
- Run as specified user.
- Also, retrieve user's group and run as this group.
-
- --group group_name
- Run as specified group.
- If both --user and --group are specified, --group must come last.
-
- -s file_name
- --status-file file_name
- Append stored TIDs to file.
- See also "burst-period" and "full-dump-period".
- If not provided, no dump ever happens.
-
- -b seconds
- --burst-period seconds
- Defines the age of last write after which an incremental write can happen.
- Such write only contain what changed since last write.
- If not provided, no incremental write is done.
-
- -F seconds
- --full-dump-period seconds
- How many seconds must separate complete dumps to status file.
- Those writes contain the complete current state.
- If both a full dump and an incremental write can happen, full dump takes
- precedence.
- If not provided, no full dump is done.
-
- -c file_name
- --config file_name
- Use given file as options file.
- It must be a python file. See sample_options.py for possible values.
- If provided and if configuration file defines base_url and
- known_tid_storage_identifier_dict variables, this program will cause
- generation of all tids before first write to status file.
-""" % {'arg0': sys.argv[0],
- 'logfile_name': Options.logfile_name,
- 'pidfile_name': Options.pidfile_name,
- 'port': Options.port,
- 'address': Options.address}
-
-class Options:
- port = 9001
- address = '0.0.0.0'
- logfile_name = 'tidstorage.log'
- pidfile_name = 'tidstorage.pid'
- fork = True
- setuid = None
- setgid = None
- status_file = None
- burst_period = None
- full_dump_period = None
- known_tid_storage_identifier_dict = {}
- base_url = None
-
-config_file_name = None
-
-options = Options()
-
-try:
- opts, args = getopt.getopt(sys.argv[1:],
- 'hnfl:p:a:s:b:F:c:',
- ['help', 'nofork', 'fg', 'log=', 'port=',
- 'address=', 'pidfile=', 'user=', 'group=',
- 'status-file=', 'burst-period=',
- 'full-dump-period=', 'config='])
-except:
- usage()
- raise
-
-for opt, arg in opts:
- if opt in ('-h', '--help'):
- usage()
- sys.exit()
- elif opt in ('-n', '--fg', '--nofork'):
- options.fork = False
- elif opt in ('-l', '--log'):
- options.logfile_name = arg
- elif opt in ('-p', '--port'):
- options.port = int(arg)
- elif opt in ('-a', '--address'):
- options.address = arg
- elif opt == '--pidfile':
- options.pidfile_name = arg
- elif opt == '--user':
- pw = pwd.getpwnam(arg)
- options.setuid = pw.pw_uid
- options.setgid = pw.pw_gid
- elif opt == '--group':
- options.setgid = grp.getgrnam(arg).gr_gid
- elif opt in ('-s', '--status-file'):
- options.status_file = arg
- elif opt in ('-b', '--burst-period'):
- options.burst_period = int(arg)
- elif opt in ('-F', '--full-dump-period'):
- options.full_dump_period = int(arg)
- elif opt in ('-c', '--config'):
- config_file_name = arg
-
-if config_file_name is not None:
- config_file = os.path.splitext(os.path.basename(config_file_name))[0]
- config_path = os.path.dirname(config_file_name)
- if len(config_path):
- config_path = [config_path]
- else:
- config_path = sys.path
- file, path, description = imp.find_module(config_file, config_path)
- module = imp.load_module(config_file, file, path, description)
- file.close()
- for option_id in [x for x in dir(Options) if x[:1] != '_']:
- if option_id not in options.__dict__ and hasattr(module, option_id):
- setattr(options, option_id, getattr(module, option_id))
-
-if options.pidfile_name is not None:
- options.pidfile_name = os.path.abspath(options.pidfile_name)
-if options.logfile_name is not None:
- options.logfile_name = os.path.abspath(options.logfile_name)
-if options.status_file is not None:
- options.status_file = os.path.abspath(options.status_file)
-
-if options.setgid is not None:
- os.setgid(options.setgid)
-
-if options.setuid is not None:
- os.setuid(options.setuid)
-
-tid_storage = TIDStorage(tid_file_path=options.status_file,
- burst_period=options.burst_period,
- full_dump_period=options.full_dump_period)
-
-signal.signal(signal.SIGHUP, HUPHandler)
-signal.signal(signal.SIGUSR1, USR1Handler)
-signal.signal(signal.SIGTERM, TERMHandler)
-
-if options.fork:
- os.chdir('/')
- os.umask(027)
- logfile = openLog()
- pidfile = open(options.pidfile_name, 'w')
- pid = os.fork()
- if pid == 0:
- os.setsid()
- pid = os.fork()
- if pid == 0:
- pidfile.close()
- os.close(0)
- os.close(1)
- os.close(2)
- sys.stdout = sys.stderr = logfile
- main(options.address, options.port)
- log('Exiting.')
- else:
- pidfile.write(str(pid))
- pidfile.close()
- os._exit(0)
- else:
- os._exit(0)
-else:
- main(options.address, options.port)
-
More information about the Erp5-report
mailing list