root / pykota / trunk / bin / edpykota @ 1257

Revision 1257, 26.1 kB (checked in by jalet, 20 years ago)

Copyright year changed.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# PyKota Print Quota Editor
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003-2004 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23# $Id$
24#
25# $Log$
26# Revision 1.64  2004/01/08 14:10:32  jalet
27# Copyright year changed.
28#
29# Revision 1.63  2003/11/24 16:50:58  jalet
30# Old help message deletedd
31#
32# Revision 1.62  2003/11/12 23:28:38  jalet
33# More work on new backend. This commit may be unstable.
34#
35# Revision 1.61  2003/11/12 13:06:35  jalet
36# Bug fix wrt no user/group name command line argument to edpykota
37#
38# Revision 1.60  2003/10/09 21:25:24  jalet
39# Multiple printer names or wildcards can be passed on the command line
40# separated with commas.
41# Beta phase.
42#
43# Revision 1.59  2003/10/07 09:07:27  jalet
44# Character encoding added to please latest version of Python
45#
46# Revision 1.58  2003/10/03 12:27:01  jalet
47# Several optimizations, especially with LDAP backend
48#
49# Revision 1.57  2003/08/20 16:01:19  jalet
50# Comment added.
51#
52# Revision 1.56  2003/07/29 20:55:17  jalet
53# 1.14 is out !
54#
55# Revision 1.55  2003/07/28 09:11:12  jalet
56# PyKota now tries to add its attributes intelligently in existing LDAP
57# directories.
58#
59# Revision 1.54  2003/07/21 06:32:42  jalet
60# Prevents email messages to be sent at modification/creation time for
61# a user/group quota
62#
63# Revision 1.53  2003/07/09 06:03:41  jalet
64# Fixed typo when using edpykota --prototype
65#
66# Revision 1.52  2003/07/07 12:11:13  jalet
67# Small fix
68#
69# Revision 1.51  2003/07/07 11:55:50  jalet
70# Small fix
71#
72# Revision 1.50  2003/07/05 12:33:53  jalet
73# More on previous fix.
74#
75# Revision 1.49  2003/07/05 12:32:07  jalet
76# Ensure that the user don't pass more than two prices for a printer.
77#
78# Revision 1.48  2003/06/25 19:52:30  jalet
79# Should be ready for testing :-)
80#
81# Revision 1.47  2003/06/25 14:10:01  jalet
82# Hey, it may work (edpykota --reset excepted) !
83#
84# Revision 1.46  2003/06/16 11:59:09  jalet
85# More work on LDAP
86#
87# Revision 1.45  2003/06/11 19:32:00  jalet
88# Severe bug wrt account balance setting should be corrected.
89#
90# Revision 1.44  2003/04/29 22:03:38  jalet
91# Better error handling.
92#
93# Revision 1.43  2003/04/23 22:13:56  jalet
94# Preliminary support for LPRng added BUT STILL UNTESTED.
95#
96# Revision 1.42  2003/04/17 13:38:47  jalet
97# Docstring corrected for better manual page
98#
99# Revision 1.41  2003/04/16 12:35:49  jalet
100# Groups quota work now !
101#
102# Revision 1.40  2003/04/16 08:22:09  jalet
103# More strict error detection.
104# Minor code rewrite to avoid some repetitive tests.
105#
106# Revision 1.39  2003/04/16 08:01:53  jalet
107# edpykota --charge command line option works now.
108#
109# Revision 1.38  2003/04/15 22:02:43  jalet
110# More complete docstring
111#
112# Revision 1.37  2003/04/15 21:58:33  jalet
113# edpykota now accepts a --delete option.
114# Preparation to allow edpykota to accept much more command line options
115# (WARNING : docstring is OK, but code isn't !)
116#
117# Revision 1.36  2003/04/15 13:55:28  jalet
118# Options --limitby and --balance added to edpykota
119#
120# Revision 1.35  2003/04/15 13:06:39  jalet
121# Allow to add a printer without any user
122#
123# Revision 1.34  2003/04/11 16:51:11  jalet
124# Bug fix for edpykota --add with users who already had a quota on the printer.
125#
126# Revision 1.33  2003/04/10 21:47:20  jalet
127# Job history added. Upgrade script neutralized for now !
128#
129# Revision 1.32  2003/04/08 21:31:39  jalet
130# (anything or 0) = anything !!! Go back to school Jerome !
131#
132# Revision 1.31  2003/04/08 21:13:44  jalet
133# Prepare --groups option to work.
134#
135# Revision 1.30  2003/04/08 21:10:18  jalet
136# Checks --groups option presence instead of --users because --users is the default.
137#
138# Revision 1.29  2003/04/05 09:28:56  jalet
139# Unnecessary message was logged
140#
141# Revision 1.28  2003/03/29 13:45:26  jalet
142# GPL paragraphs were incorrectly (from memory) copied into the sources.
143# Two README files were added.
144# Upgrade script for PostgreSQL pre 1.01 schema was added.
145#
146# Revision 1.27  2003/03/10 00:23:04  jalet
147# Bad english
148#
149# Revision 1.26  2003/03/10 00:11:27  jalet
150# Cleaner example.
151#
152# Revision 1.25  2003/03/09 23:56:21  jalet
153# Option noquota added to do accounting only.
154#
155# Revision 1.24  2003/02/27 23:48:41  jalet
156# Correctly maps PyKota's log levels to syslog log levels
157#
158# Revision 1.23  2003/02/27 22:55:20  jalet
159# WARN log priority doesn't exist.
160#
161# Revision 1.22  2003/02/27 09:37:02  jalet
162# Wildcards seem to work now
163#
164# Revision 1.21  2003/02/27 09:04:46  jalet
165# user and group names can be passed as wildcards if the --add option
166# is not set. The default is to act on all users or groups.
167#
168# Revision 1.20  2003/02/10 12:07:30  jalet
169# Now repykota should output the recorded total page number for each printer too.
170#
171# Revision 1.19  2003/02/09 13:40:29  jalet
172# typo
173#
174# Revision 1.18  2003/02/09 12:56:53  jalet
175# Internationalization begins...
176#
177# Revision 1.17  2003/02/08 22:47:23  jalet
178# Option --reset can now be used without having to use soft and hard limits
179# on the command line.
180#
181# Revision 1.16  2003/02/08 22:39:46  jalet
182# --reset command line option added
183#
184# Revision 1.15  2003/02/08 22:20:01  jalet
185# Clarification on why we don't check with /etc/passwd to see if the user
186# name is valid or not.
187#
188# Revision 1.14  2003/02/08 22:18:15  jalet
189# Now checks user and group names for validity before adding them
190#
191# Revision 1.13  2003/02/08 22:09:02  jalet
192# Only printer was added the first time.
193#
194# Revision 1.12  2003/02/08 21:44:49  jalet
195# Python 2.1 string module doesn't define ascii_letters
196#
197# Revision 1.11  2003/02/08 09:42:44  jalet
198# Better handle wrong or bad command line arguments
199#
200# Revision 1.10  2003/02/08 09:39:20  jalet
201# typos
202#
203# Revision 1.9  2003/02/08 09:38:06  jalet
204# Badly placed test
205#
206# Revision 1.8  2003/02/07 22:53:57  jalet
207# Checks if printer name is valid before adding it
208#
209# Revision 1.7  2003/02/07 22:17:58  jalet
210# Incomplete test
211#
212# Revision 1.6  2003/02/07 22:13:13  jalet
213# Perhaps edpykota is now able to add printers !!! Oh, stupid me !
214#
215# Revision 1.5  2003/02/06 14:49:04  jalet
216# edpykota should be ok now
217#
218# Revision 1.4  2003/02/06 14:28:59  jalet
219# edpykota should be ok, minus some typos
220#
221# Revision 1.3  2003/02/06 10:47:21  jalet
222# Documentation string and command line options didn't match.
223#
224# Revision 1.2  2003/02/06 10:39:23  jalet
225# Preliminary edpykota work.
226#
227# Revision 1.1  2003/02/05 21:41:09  jalet
228# Skeletons added for all command line tools
229#
230#
231#
232
233import sys
234
235from pykota import version
236from pykota.tool import PyKotaTool, PyKotaToolError
237from pykota.config import PyKotaConfigError
238from pykota.storage import PyKotaStorageError
239
240__doc__ = """edpykota v%s (c) 2003-2004 C@LL - Conseil Internet & Logiciels Libres
241A Print Quota editor for PyKota.
242
243command line usage :
244
245  edpykota [options] user1 user2 ... userN
246 
247  edpykota [options] group1 group2 ... groupN
248
249options :
250
251  -v | --version       Prints edpykota's version number then exits.
252  -h | --help          Prints this message then exits.
253 
254  -a | --add           Adds users and/or printers if they don't
255                       exist on the Quota Storage Server.
256                       
257  -d | --delete        Deletes users/groups from the quota storage.
258                       Printers are never deleted.
259                       
260  -c | --charge p[,j]  Sets the price per page and per job to charge
261                       for a particular printer. Job price is optional.
262                       If both are to be set, separate them with a comma.
263                       Floating point values are allowed.
264 
265  -i | --ingroups g1[,g2...]  Puts the users into each of the groups
266                              listed, separated by commas. The groups
267                              must already exist in the Quota Storage.
268 
269  -u | --users         Edit users print quotas, this is the default.
270 
271  -P | --printer p     Edit quotas on printer p only. Actually p can
272                       use wildcards characters to select only
273                       some printers. The default value is *, meaning
274                       all printers.
275                       You can specify several names or wildcards,
276                       by separating them with commas.
277 
278  -g | --groups        Edit groups print quotas instead of users.
279                         
280  -p | --prototype u|g Uses user u or group g as a prototype to set
281                       print quotas
282                       
283  -n | --noquota       Doesn't set a quota but only does accounting.
284 
285  -r | --reset         Resets the printed page counter for the user
286                       or group to zero. The life time page counter
287                       is kept unchanged.
288                       
289  -l | --limitby l     Choose if the user/group is limited in printing                     
290                       by its account balance or by its page quota.
291                       The default value is 'quota'. Allowed values
292                       are 'quota' and 'balance'.
293                       
294  -b | --balance b     Sets the user's account balance to b.                     
295                       Account balance may be increase or decreased
296                       if b is prefixed with + or -.
297                       WARNING : when decreasing account balance,
298                       the total paid so far by the user is decreased
299                       too.
300                       Groups don't have a real balance, but the
301                       sum of their users' account balance.
302                       
303  -S | --softlimit sl  Sets the quota soft limit to sl pages.                       
304 
305  -H | --hardlimit hl  Sets the quota hard limit to hl pages.
306 
307  user1 through userN and group1 through groupN can use wildcards
308  if the --add option is not set.
309 
310examples :                             
311
312  $ edpykota -p jerome john paul george ringo
313 
314  This will set print quotas for the users john, paul, george and ringo
315  to the same values than user jerome. User jerome must exist.
316 
317  $ edpykota --printer lp -S 50 -H 60 jerome
318 
319  This will set jerome's print quota on the lp printer to a soft limit
320  of 50 pages, and a hard limit of 60 pages. If either user jerome or
321  printer lp doesn't exist on the Quota Storage Server then nothing is done.
322
323  $ edpykota --add --printer lp --ingroups coders,it -S 50 -H 60 jerome
324 
325  Same as above, but if either user jerome or printer lp doesn't exist
326  on the Quota Storage Server they are automatically added. Also
327  user jerome is put into the groups "coders" and "it" which must
328  already exist in the Quota Storage.
329           
330  $ edpykota -g -S 500 -H 550 financial support           
331 
332  This will set print quota soft limit to 500 pages and hard limit
333  to 550 pages for groups financial and support on all printers.
334 
335  $ edpykota --reset jerome "jo*"
336 
337  This will reset jerome's page counter to zero on all printers, as
338  well as every user whose name begins with 'jo'.
339  Their life time page counter on each printer will be kept unchanged.
340 
341  $ edpykota --printer hpcolor --noquota jerome
342 
343  This will tell PyKota to not limit jerome when printing on the
344  hpcolor printer. All his jobs will be allowed on this printer, but
345  accounting of the pages he prints will still be kept.
346  Print Quotas for jerome on other printers are unchanged.
347 
348  $ edpykota --limitby balance jerome
349 
350  This will tell PyKota to limit jerome by his account's balance
351  when printing.
352 
353  $ edpykota --balance +10.0 jerome
354 
355  This will increase jerome's account balance by 10.0 (in your
356  own currency). You can decrease the account balance with a
357  dash prefix, and set it to a fixed amount with no prefix.
358 
359  $ edpykota --delete jerome rachel
360 
361  This will completely delete jerome and rachel from the Quota Storage
362  database. All their quotas and jobs will be deleted too.
363 
364  $ edpykota --printer lp --charge 0.1
365 
366  This will set the page price for printer lp to 0.1. Job price
367  will not be changed.
368
369This program is free software; you can redistribute it and/or modify
370it under the terms of the GNU General Public License as published by
371the Free Software Foundation; either version 2 of the License, or
372(at your option) any later version.
373
374This program is distributed in the hope that it will be useful,
375but WITHOUT ANY WARRANTY; without even the implied warranty of
376MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
377GNU General Public License for more details.
378
379You should have received a copy of the GNU General Public License
380along with this program; if not, write to the Free Software
381Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
382
383Please e-mail bugs to: %s""" % (version.__version__, version.__author__)
384       
385class EdPyKota(PyKotaTool) :       
386    """A class for edpykota."""
387    def main(self, names, options) :
388        """Edit user or group quotas."""
389       
390        suffix = (options["groups"] and "Group") or "User"       
391       
392        softlimit = hardlimit = None   
393        if not options["noquota"] :
394            if options["softlimit"] :
395                try :
396                    softlimit = int(options["softlimit"].strip())
397                except ValueError :   
398                    raise PyKotaToolError, _("Invalid softlimit value %s.") % options["softlimit"]
399            if options["hardlimit"] :
400                try :
401                    hardlimit = int(options["hardlimit"].strip())
402                except ValueError :   
403                    raise PyKotaToolError, _("Invalid hardlimit value %s.") % options["hardlimit"]
404            if (softlimit is not None) and (hardlimit is not None) and (hardlimit < softlimit) :       
405                # error, exchange them
406                self.logger.log_message(_("Hard limit %i is less than soft limit %i, values will be exchanged.") % (hardlimit, softlimit))
407                (softlimit, hardlimit) = (hardlimit, softlimit)
408           
409        balance = options["balance"]
410        if balance :
411            balance = balance.strip()
412            try :
413                balancevalue = float(balance)
414            except ValueError :   
415                raise PyKotaToolError, _("Invalid balance value %s" % options["balance"])
416           
417        if options["charge"] :
418            try :
419                charges = [float(part) for part in options["charge"].split(',', 1)]
420            except ValueError :   
421                raise PyKotaToolError, _("Invalid charge amount value %s" % options["charge"])
422            else :   
423                if len(charges) > 2 :
424                    charges = charges[:2]
425                if len(charges) != 2 :
426                    charges = [charges[0], None]
427                   
428        limitby = options["limitby"]
429        if limitby :
430            limitby = limitby.strip().lower()
431        if limitby and (limitby not in ('quota', 'balance')) :   
432            raise PyKotaToolError, _("Invalid limitby value %s" % options["limitby"])
433           
434        if options["ingroups"] :   
435            groupnames = [gname.strip() for gname in options["ingroups"].split(',')]
436        else :   
437            groupnames = []
438           
439        if options["prototype"] :   
440            protoentry = getattr(self.storage, "get%s" % suffix)(options["prototype"])
441           
442        printeradded = 0
443        printers = self.storage.getMatchingPrinters(options["printer"])
444        if not printers :
445            pname = options["printer"]
446            if options["add"] and pname :
447                if self.isValidName(pname) :
448                    printers = [ self.storage.addPrinter(pname) ]
449                    if printers[0].Exists :
450                        printeradded = 1
451                    else :   
452                        raise PyKotaToolError, _("Impossible to add printer %s") % pname
453                else :   
454                    raise PyKotaToolError, _("Invalid printer name %s") % pname
455            else :
456                raise PyKotaToolError, _("There's no printer matching %s") % pname
457        if not names :   
458            if options["add"] :
459                if not printeradded :
460                    raise PyKotaToolError, _("You have to pass user or group names on the command line")
461                else :   
462                    names = getattr(self.storage, "getAll%ssNames" % suffix)()
463            else :   
464                names = [ "*" ] # all users
465               
466        changed = {} # tracks changes made at the user/group level
467        for printer in printers :
468            if options["charge"] :
469                (perpage, perjob) = charges
470                printer.setPrices(perpage, perjob)   
471               
472            if options["prototype"] :
473                if protoentry.Exists :
474                    protoquota = getattr(self.storage, "get%sPQuota" % suffix)(protoentry, printer)
475                    if not protoquota.Exists :
476                        self.logger.log_message(_("Prototype %s not found in Quota Storage for printer %s.") % (protoentry.Name, printer.Name))
477                        continue    # skip this printer
478                    else :   
479                        (softlimit, hardlimit) = (protoquota.SoftLimit, protoquota.HardLimit)
480                else :       
481                    self.logger.log_message(_("Prototype object %s not found in Quota Storage.") % protoentry.Name)
482                   
483            if not options["noquota"] :   
484                if hardlimit is None :   
485                    hardlimit = softlimit
486                    if hardlimit is not None :
487                        self.logger.log_message(_("Undefined hard limit set to soft limit (%s) on printer %s.") % (str(hardlimit), printer.Name))
488                if softlimit is None :   
489                    softlimit = hardlimit
490                    if softlimit is not None :
491                        self.logger.log_message(_("Undefined soft limit set to hard limit (%s) on printer %s.") % (str(softlimit), printer.Name))
492                       
493            if options["add"] :   
494                allentries = []   
495                for name in names :
496                    entry = getattr(self.storage, "get%s" % suffix)(name)
497                    entrypquota = getattr(self.storage, "get%sPQuota" % suffix)(entry, printer)
498                    allentries.append((entry, entrypquota))
499            else :   
500                allentries = getattr(self.storage, "getPrinter%ssAndQuotas" % suffix)(printer, names)
501               
502            for (entry, entrypquota) in allentries :
503                if not changed.has_key(entry.Name) :
504                    changed[entry.Name] = {}
505                    if not options["groups"] :
506                        changed[entry.Name]["ingroups"] = []
507                       
508                if not entry.Exists :       
509                    # not found
510                    if options["add"] :
511                        # In case we want to add something, it is crucial
512                        # that we DON'T check with the system accounts files
513                        # like /etc/passwd because users may be defined
514                        # only remotely
515                        if self.isValidName(entry.Name) :
516                            entry = getattr(self.storage, "add%s" % suffix)(entry)
517                        else :   
518                            if options["groups"] :
519                                self.logger.log_message(_("Invalid group name %s") % entry.Name)
520                            else :   
521                                self.logger.log_message(_("Invalid user name %s") % entry.Name)
522                               
523                if entry.Exists and (not entrypquota.Exists) :
524                    # not found
525                    if options["add"] :
526                        entrypquota = getattr(self.storage, "add%sPQuota" % suffix)(entry, printer)
527                       
528                if not entrypquota.Exists :     
529                    self.logger.log_message(_("Quota not found for object %s on printer %s.") % (entry.Name, printer.Name))
530                else :   
531                    if options["delete"] :
532                        entry.delete()
533                    else :
534                        if options["noquota"] or options["prototype"] or ((softlimit is not None) and (hardlimit is not None)) :
535                            entrypquota.setLimits(softlimit, hardlimit)
536                        if limitby :
537                            if changed[entry.Name].get("limitby") is None :
538                                entry.setLimitBy(limitby)
539                                changed[entry.Name]["limitby"] = limitby
540                       
541                        if not options["groups"] :
542                            if options["reset"] :
543                                entrypquota.reset()
544                               
545                            if balance :
546                                if changed[entry.Name].get("balance") is None :
547                                    if balance.startswith("+") or balance.startswith("-") :
548                                        newbalance = float(entry.AccountBalance or 0.0) + balancevalue
549                                        newlifetimepaid = float(entry.LifeTimePaid or 0.0) + balancevalue
550                                        entry.setAccountBalance(newbalance, newlifetimepaid)
551                                    else :
552                                        diff = balancevalue - float(entry.AccountBalance or 0.0)
553                                        newlifetimepaid = float(entry.LifeTimePaid or 0.0) + diff
554                                        entry.setAccountBalance(balancevalue, newlifetimepaid)
555                                    changed[entry.Name]["balance"] = balance
556                                   
557                            for groupname in groupnames :       
558                                # not executed if option --ingroups is not used
559                                if groupname not in changed[entry.Name]["ingroups"] :
560                                    group = self.storage.getGroup(groupname)
561                                    if group.Exists :
562                                        self.storage.addUserToGroup(entry, group)
563                                        changed[entry.Name]["ingroups"].append(groupname)
564                                    else :
565                                        self.logger.log_message(_("Group %s not found in the PyKota Storage.") % groupname)
566                                       
567                        # This line disabled to prevent sending of unwanted email                 
568                        # messages if quota is reached at creation/modification time.
569                        # The check will be done at print time anyway.
570                        # getattr(self, "warn%sPQuota" % suffix)(entrypquota)   
571                     
572if __name__ == "__main__" : 
573    retcode = 0
574    try :
575        defaults = { \
576                     "printer" : "*", \
577                   }
578        short_options = "vhdc:l:b:i:naugrp:P:S:H:"
579        long_options = ["help", "version", "charge=", "delete", "limitby=", "balance=", "ingroups=", "noquota", "add", "users", "groups", "reset", "prototype=", "printer=", "softlimit=", "hardlimit="]
580       
581        # Initializes the command line tool
582        editor = EdPyKota(doc=__doc__)
583       
584        # parse and checks the command line
585        (options, args) = editor.parseCommandline(sys.argv[1:], short_options, long_options)
586       
587        # sets long options
588        options["help"] = options["h"] or options["help"]
589        options["version"] = options["v"] or options["version"]
590        options["add"] = options["a"] or options["add"]
591        options["users"] = options["u"] or options["users"]
592        options["groups"] = options["g"] or options["groups"]
593        options["prototype"] = options["p"] or options["prototype"]
594        options["printer"] = options["P"] or options["printer"] or defaults["printer"]
595        options["softlimit"] = options["S"] or options["softlimit"]
596        options["hardlimit"] = options["H"] or options["hardlimit"] 
597        options["reset"] = options["r"] or options["reset"] 
598        options["noquota"] = options["n"] or options["noquota"]
599        options["limitby"] = options["l"] or options["limitby"]
600        options["balance"] = options["b"] or options["balance"] 
601        options["delete"] = options["d"] or options["delete"] 
602        options["ingroups"] = options["i"] or options["ingroups"]
603        options["charge"] = options["c"] or options["charge"]
604       
605        if options["help"] :
606            editor.display_usage_and_quit()
607        elif options["version"] :
608            editor.display_version_and_quit()
609        elif options["users"] and options["groups"] :   
610            raise PyKotaToolError, _("incompatible options, see help.")
611        elif (options["add"] or options["prototype"]) and options["delete"] :   
612            raise PyKotaToolError, _("incompatible options, see help.")
613        elif (options["softlimit"] or options["hardlimit"]) and options["prototype"] :   
614            raise PyKotaToolError, _("incompatible options, see help.")
615        elif options["noquota"] and (options["prototype"] or options["hardlimit"] or options["softlimit"]) :
616            raise PyKotaToolError, _("incompatible options, see help.")
617        elif options["groups"] and (options["balance"] or options["ingroups"]) :
618            raise PyKotaToolError, _("incompatible options, see help.")
619        else :
620            retcode = editor.main(args, options)
621    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError), msg :           
622        sys.stderr.write("%s\n" % msg)
623        sys.stderr.flush()
624        retcode = -1
625
626    try :
627        editor.storage.close()
628    except (TypeError, NameError, AttributeError) :   
629        pass
630       
631    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.