root / pykota / trunk / bin / edpykota @ 2454

Revision 2454, 29.7 kB (checked in by jerome, 19 years ago)

Added the -I | --increase command line switch to edpykota

  • 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, 2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22#
23# $Id$
24#
25#
26
27import sys
28import os
29import pwd
30import grp
31from pykota.tool import PyKotaTool, PyKotaToolError, crashed, N_
32from pykota.config import PyKotaConfigError
33from pykota.storage import PyKotaStorageError
34
35__doc__ = N_("""edpykota v%(__version__)s (c) %(__years__)s %(__author__)s
36
37A Print Quota editor for PyKota.
38
39command line usage :
40
41  edpykota [options] user1 user2 ... userN
42 
43  edpykota [options] group1 group2 ... groupN
44
45options :
46
47  -v | --version       Prints edpykota's version number then exits.
48  -h | --help          Prints this message then exits.
49 
50  -a | --add           Adds users and/or printers if they don't
51                       exist on the Quota Storage Server.
52                       
53  -d | --delete        Deletes users/groups from the quota storage.
54                       Printers are never deleted.
55                       
56  -c | --charge p[,j]  Sets the price per page and per job to charge
57                       for a particular printer. Job price is optional.
58                       If both are to be set, separate them with a comma.
59                       Floating point values are allowed.
60                       
61  -o | --overcharge f  Sets the overcharging factor applied to the user
62                       when computing the cost of a print job. Positive or
63                       negative floating point values are allowed,
64                       this allows you to do some really creative
65                       things like giving money to an user whenever
66                       he prints. The number of pages in a print job
67                       is not modified by this coefficient, only the
68                       cost of the job for a particular user.
69                       Only users have a coefficient.
70 
71  -i | --ingroups g1[,g2...]  Puts the users into each of the groups
72                              listed, separated by commas. The groups
73                              must already exist in the Quota Storage.
74 
75  -u | --users         Edit users print quotas, this is the default.
76 
77  -P | --printer p     Edit quotas on printer p only. Actually p can
78                       use wildcards characters to select only
79                       some printers. The default value is *, meaning
80                       all printers.
81                       You can specify several names or wildcards,
82                       by separating them with commas.
83 
84  -G | --pgroups pg1[,pg2...] Adds the printer(s) to the printer groups
85                       pg1, pg2, etc... which must already exist.
86                       A printer group is just like a normal printer,
87                       only that it is usually unknown from the printing
88                       system. Create printer groups exactly the same
89                       way that you create printers, then add other
90                       printers to them with this option.
91                       Accounting is done on a printer and on all
92                       the printer groups it belongs to, quota checking
93                       is done on a printer and on all the printer groups
94                       it belongs to.
95 
96  -g | --groups        Edit users groups print quotas instead of users.
97                         
98  -p | --prototype u|g Uses user u or group g as a prototype to set
99                       print quotas
100                       
101  -n | --noquota       Doesn't set a quota but only does accounting.
102                       This is the same as --limitby noquota.
103 
104  -r | --reset         Resets the actual page counter for the user
105                       or group to zero on the specified printers.
106                       The life time page counter is kept unchanged.
107                       
108  -R | --hardreset     Resets the actual and life time page counters
109                       for the user or group to zero on the specified
110                       printers. This is a shortcut for '--used 0'.
111                       
112  -l | --limitby l     Choose if the user/group is limited in printing                     
113                       by its account balance or by its page quota.
114                       The default value is 'quota'. Allowed values
115                       are 'quota' 'balance' 'noquota' 'noprint' 
116                       and 'nochange' :
117                       
118                         - quota : limit by number of pages per printer.
119                         - balance : limit by number of credits in account.
120                         - noquota : no limit, accounting still done.
121                         - nochange : no limit, accounting not done.
122                         - noprint : printing is denied.
123                       NB : nochange and noprint are not supported for groups.
124                       
125  -b | --balance b     Sets the user's account balance to b.                     
126                       Account balance may be increase or decreased
127                       if b is prefixed with + or -.
128                       WARNING : when decreasing account balance,
129                       the total paid so far by the user is decreased
130                       too.
131                       Groups don't have a real balance, but the
132                       sum of their users' account balance.
133                       
134  -C | --comment txt   Defines some informational text to be associated
135                       with a change to an user's account balance.
136                       Only meaningful if -b | --balance is also used.
137                       
138  -S | --softlimit sl  Sets the quota soft limit to sl pages.                       
139 
140  -H | --hardlimit hl  Sets the quota hard limit to hl pages.
141 
142  -I | --increase v    Increase both Soft and Hard limits by the value
143                       of v. You can prefix v with + or -, if no sign is
144                       used, + is assumed.
145
146  -U | --used usage    Sets the pagecounters for the user to usage pages;
147                       useful for migrating users from a different system
148                       where they have already used some pages. Actual
149                       and Life Time page counters may be increased or decreased
150                       if usage is prefixed with + or -.
151                       WARNING : BOTH page counters are modified in all cases,
152                       so be careful.
153                       NB : if 'usage' equals '0', then the action taken is
154                       the same as if --hardreset was used.
155
156  user1 through userN and group1 through groupN can use wildcards
157  if the --add option is not set.
158 
159examples :                             
160
161  $ edpykota --add -p jerome john paul george ringo/ringo@example.com
162 
163  This will add users john, paul, george and ringo to the quota
164  database, and set their print quotas to the same values than user
165  jerome. User jerome must already exist.
166  User ringo's email address will also be set to 'ringo@example.com'
167 
168  $ edpykota --printer lp -S 50 -H 60 jerome
169 
170  This will set jerome's print quota on the lp printer to a soft limit
171  of 50 pages, and a hard limit of 60 pages. If either user jerome or
172  printer lp doesn't exist on the Quota Storage Server then nothing is done.
173
174  $ edpykota --add --printer lp --ingroups coders,it -S 50 -H 60 jerome
175 
176  Same as above, but if either user jerome or printer lp doesn't exist
177  on the Quota Storage Server they are automatically added. Also
178  user jerome is put into the groups "coders" and "it" which must
179  already exist in the Quota Storage.
180           
181  $ edpykota -g -S 500 -H 550 financial support           
182 
183  This will set print quota soft limit to 500 pages and hard limit
184  to 550 pages for groups financial and support on all printers.
185 
186  $ edpykota --reset jerome "jo*"
187 
188  This will reset jerome's page counter to zero on all printers, as
189  well as every user whose name begins with 'jo'.
190  Their life time page counter on each printer will be kept unchanged.
191  You can also reset the life time page counters by using the
192  --hardreset | -R command line option.
193 
194  $ edpykota --printer hpcolor --noquota jerome
195 
196  This will tell PyKota to not limit jerome when printing on the
197  hpcolor printer. All his jobs will be allowed on this printer, but
198  accounting of the pages he prints will still be kept.
199  Print Quotas for jerome on other printers are unchanged.
200 
201  $ edpykota --limitby balance jerome
202 
203  This will tell PyKota to limit jerome by his account's balance
204  when printing.
205 
206  $ edpykota --balance +10.0 jerome
207 
208  This will increase jerome's account balance by 10.0 (in your
209  own currency). You can decrease the account balance with a
210  dash prefix, and set it to a fixed amount with no prefix.
211 
212  $ edpykota --delete jerome rachel
213 
214  This will completely delete jerome and rachel from the Quota Storage
215  database. All their quotas and jobs will be deleted too.
216 
217  $ edpykota --printer lp --charge 0.1
218 
219  This will set the page price for printer lp to 0.1. Job price
220  will not be changed.
221 
222  $ edpykota --printer hplj1,hplj2 --pgroups Laser,HP
223 
224  This will put printers hplj1 and hplj2 in printers groups Laser and HP.
225  When printing either on hplj1 or hplj2, print quota will also be
226  checked and accounted for on virtual printers Laser and HP.
227 
228  $ edpykota --overcharge 2.5 poorstudent
229 
230  This will overcharge the poorstudent user by a factor of 2.5.
231 
232  $ edpykota --overcharge -1 jerome
233 
234  User jerome will actually earn money whenever he prints.
235 
236  $ edpykota --overcharge 0 boss
237 
238  User boss can print at will, it won't cost him anything because the
239  cost of each print job will be multiplied by zero before charging
240  his account.
241""") 
242       
243class EdPyKota(PyKotaTool) :       
244    """A class for edpykota."""
245    def main(self, names, options) :
246        """Edit user or group quotas."""
247        if not self.config.isAdmin :
248            raise PyKotaToolError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
249       
250        suffix = (options["groups"] and "Group") or "User"       
251       
252        softlimit = hardlimit = None
253       
254        if options["noquota"] :
255            options["limitby"] = "noquota"
256           
257        limitby = options["limitby"]
258        if limitby :
259            limitby = limitby.strip().lower()
260        if limitby :
261            if limitby not in ('quota', 'balance', 'noquota', \
262                                        'noprint', 'nochange') :
263                raise PyKotaToolError, _("Invalid limitby value %s") % options["limitby"]
264            if limitby in ('noquota', 'nochange') :   
265                options["noquota"] = 1
266            if (limitby in ('nochange', 'noprint')) and options["groups"] :   
267                raise PyKotaToolError, _("Invalid limitby value %s") % options["limitby"]
268
269        used = options["used"]
270        if used :
271            used = used.strip()
272            try :
273                int(used)
274            except ValueError :
275                raise PyKotaToolError, _("Invalid used value %s.") % used
276               
277        increase = options["increase"]
278        if increase :
279            try :
280                increase = int(increase.strip())
281            except ValueError :
282                raise PyKotaToolError, _("Invalid increase value %s.") % increase
283
284        if not options["noquota"] :
285            if options["softlimit"] :
286                try :
287                    softlimit = int(options["softlimit"].strip())
288                    if softlimit < 0 :
289                        raise ValueError
290                except ValueError :   
291                    raise PyKotaToolError, _("Invalid softlimit value %s.") % options["softlimit"]
292            if options["hardlimit"] :
293                try :
294                    hardlimit = int(options["hardlimit"].strip())
295                    if hardlimit < 0 :
296                        raise ValueError
297                except ValueError :   
298                    raise PyKotaToolError, _("Invalid hardlimit value %s.") % options["hardlimit"]
299            if (softlimit is not None) and (hardlimit is not None) and (hardlimit < softlimit) :       
300                # error, exchange them
301                self.printInfo(_("Hard limit %i is less than soft limit %i, values will be exchanged.") % (hardlimit, softlimit))
302                (softlimit, hardlimit) = (hardlimit, softlimit)
303           
304        overcharge = options["overcharge"]
305        if overcharge :
306            try :
307                overcharge = float(overcharge.strip())
308            except (ValueError, AttributeError) :   
309                raise PyKotaToolError, _("Invalid overcharge value %s") % options["overcharge"]
310               
311        balance = options["balance"]
312        if balance :
313            balance = balance.strip()
314            try :
315                balancevalue = float(balance)
316            except ValueError :   
317                raise PyKotaToolError, _("Invalid balance value %s") % options["balance"]
318           
319        if options["charge"] :
320            try :
321                charges = [float(part) for part in options["charge"].split(',', 1)]
322            except ValueError :   
323                raise PyKotaToolError, _("Invalid charge amount value %s") % options["charge"]
324            else :   
325                if len(charges) > 2 :
326                    charges = charges[:2]
327                if len(charges) != 2 :
328                    charges = [charges[0], None]
329                   
330        if options["ingroups"] :   
331            groupnames = [gname.strip() for gname in options["ingroups"].split(',')]
332        else :   
333            groupnames = []
334           
335        rejectunknown = self.config.getRejectUnknown()   
336        printeradded = 0
337        printers = self.storage.getMatchingPrinters(options["printer"])
338        if not printers :
339            pname = options["printer"]
340            if options["add"] and pname :
341                if self.isValidName(pname) :
342                    printers = [ self.storage.addPrinter(pname) ]
343                    if printers[0].Exists :
344                        printeradded = 1
345                    else :   
346                        raise PyKotaToolError, _("Impossible to add printer %s") % pname
347                else :   
348                    raise PyKotaToolError, _("Invalid printer name %s") % pname
349            else :
350                raise PyKotaToolError, _("There's no printer matching %s") % pname
351        if not names :   
352            if options["delete"] :   
353                raise PyKotaToolError, _("You have to pass user or group names on the command line")
354            else :
355                names = getattr(self.storage, "getAll%ssNames" % suffix)() # all users or groups
356               
357        printersgroups = []       
358        if options["pgroups"] :       
359            printersgroups = self.storage.getMatchingPrinters(options["pgroups"])
360           
361        if options["prototype"] :   
362            protoentry = getattr(self.storage, "get%s" % suffix)(options["prototype"])
363            if not protoentry.Exists :
364                raise PyKotaToolError, _("Prototype object %s not found in Quota Storage.") % protoentry.Name
365            else :   
366                limitby = protoentry.LimitBy
367                balancevalue = protoentry.AccountBalance
368                if balancevalue is not None :
369                    balance = str(abs(balancevalue))
370                else :   
371                    balance = None
372                overcharge = getattr(protoentry, "OverCharge", None)
373           
374        missingusers = {}
375        missinggroups = {}   
376        todelete = {}   
377        changed = {} # tracks changes made at the user/group level
378        for printer in printers :
379            for pgroup in printersgroups :
380                pgroup.addPrinterToGroup(printer)   
381               
382            if options["charge"] :
383                (perpage, perjob) = charges
384                printer.setPrices(perpage, perjob)   
385               
386            if options["prototype"] :
387                protoquota = getattr(self.storage, "get%sPQuota" % suffix)(protoentry, printer)
388                if not protoquota.Exists :
389                    self.printInfo(_("Prototype %s not found in Quota Storage for printer %s.") % (protoentry.Name, printer.Name))
390                else :   
391                    (softlimit, hardlimit) = (protoquota.SoftLimit, protoquota.HardLimit)
392                   
393            if not options["noquota"] :   
394                if hardlimit is None :   
395                    hardlimit = softlimit
396                    if hardlimit is not None :
397                        self.printInfo(_("Undefined hard limit set to soft limit (%s) on printer %s.") % (str(hardlimit), printer.Name))
398                if softlimit is None :   
399                    softlimit = hardlimit
400                    if softlimit is not None :
401                        self.printInfo(_("Undefined soft limit set to hard limit (%s) on printer %s.") % (str(softlimit), printer.Name))
402                       
403            if options["add"] :   
404                allentries = []   
405                for name in names :
406                    email = ""
407                    if not options["groups"] :
408                        splitname = name.split('/', 1)     # username/email
409                        if len(splitname) == 1 :
410                            splitname.append("")
411                        (name, email) = splitname
412                        if email and (email.count('@') != 1) :
413                            self.printInfo(_("Invalid email address %s") % email)
414                            email = ""
415                    entry = getattr(self.storage, "get%s" % suffix)(name)
416                    if email and not options["groups"] :
417                        entry.Email = email
418                    entrypquota = getattr(self.storage, "get%sPQuota" % suffix)(entry, printer)
419                    allentries.append((entry, entrypquota))
420            else :   
421                allentries = getattr(self.storage, "getPrinter%ssAndQuotas" % suffix)(printer, names)
422               
423            # TODO : do this only once !!!   
424            allnames = [entry.Name for (entry, dummy) in allentries]
425            for name in names :   
426                if not self.matchString(name, allnames) :
427                    if options["groups"] :
428                        missinggroups[name] = 1
429                    else :   
430                        missingusers[name] = 1
431               
432            for (entry, entrypquota) in allentries :
433                if not changed.has_key(entry.Name) :
434                    changed[entry.Name] = {}
435                    if not options["groups"] :
436                        changed[entry.Name]["ingroups"] = []
437                       
438                if not entry.Exists :       
439                    # not found
440                    if options["add"] :
441                        # In case we want to add something, it is crucial
442                        # that we DON'T check with the system accounts files
443                        # like /etc/passwd because users may be defined
444                        # only remotely
445                        if self.isValidName(entry.Name) :
446                            reject = 0
447                            if rejectunknown :
448                                if options["groups"] :
449                                    try :
450                                        grp.getgrnam(entry.Name)
451                                    except KeyError :   
452                                        self.printInfo(_("Unknown group %s") % entry.Name, "error")
453                                        reject = 1
454                                else :   
455                                    try :
456                                        pwd.getpwnam(entry.Name)
457                                    except KeyError :   
458                                        self.printInfo(_("Unknown user %s") % entry.Name, "error")
459                                        reject = 1
460                            if not reject :       
461                                entry = getattr(self.storage, "add%s" % suffix)(entry)
462                        else :   
463                            if options["groups"] :
464                                self.printInfo(_("Invalid group name %s") % entry.Name)
465                            else :   
466                                self.printInfo(_("Invalid user name %s") % entry.Name)
467                    else :
468                        if options["groups"] :
469                            missinggroups[entry.Name] = 1
470                        else :   
471                            missingusers[entry.Name] = 1
472                elif options["delete"] :               
473                    todelete[entry.Name] = entry
474                               
475                if entry.Exists and (not entrypquota.Exists) :
476                    # not found
477                    if options["add"] :
478                        entrypquota = getattr(self.storage, "add%sPQuota" % suffix)(entry, printer)
479                       
480                if not entrypquota.Exists :     
481                    self.printInfo(_("Quota not found for object %s on printer %s.") % (entry.Name, printer.Name))
482                else :   
483                    if options["noquota"] or options["prototype"] \
484                       or ((softlimit is not None) and (hardlimit is not None)) :
485                        entrypquota.setLimits(softlimit, hardlimit)
486                    if increase :
487                       if (entrypquota.SoftLimit is None) \
488                           or (entrypquota.HardLimit is None) :
489                           self.printInfo(_("You can't increase limits by %s when no limit is set.") % increase, "error")
490                       else :
491                           newsoft = entrypquota.SoftLimit + increase         
492                           newhard = entrypquota.HardLimit + increase         
493                           if (newsoft >= 0) and (newhard >= 0) :
494                               entrypquota.setLimits(newsoft, newhard)
495                           else :   
496                               self.printInfo(_("You can't set negative limits."), "error")
497                    if limitby :
498                        if changed[entry.Name].get("limitby") is None :
499                            entry.setLimitBy(limitby)
500                            changed[entry.Name]["limitby"] = limitby
501                   
502                    if options["reset"] :
503                        entrypquota.reset()
504                       
505                    if options["hardreset"] :   
506                        entrypquota.hardreset()
507                       
508                    if not options["groups"] :
509                        if used :
510                            entrypquota.setUsage(used)
511                           
512                        if overcharge is not None :   
513                            if changed[entry.Name].get("overcharge") is None :
514                                entry.setOverChargeFactor(overcharge)
515                                changed[entry.Name]["overcharge"] = overcharge
516                               
517                        if balance :
518                            if changed[entry.Name].get("balance") is None :
519                                if balance.startswith("+") or balance.startswith("-") :
520                                    newbalance = float(entry.AccountBalance or 0.0) + balancevalue
521                                    newlifetimepaid = float(entry.LifeTimePaid or 0.0) + balancevalue
522                                    entry.setAccountBalance(newbalance, newlifetimepaid, options["comment"])
523                                else :
524                                    diff = balancevalue - float(entry.AccountBalance or 0.0)
525                                    newlifetimepaid = float(entry.LifeTimePaid or 0.0) + diff
526                                    entry.setAccountBalance(balancevalue, newlifetimepaid, options["comment"])
527                                changed[entry.Name]["balance"] = balance
528                               
529                        for groupname in groupnames :       
530                            # not executed if option --ingroups is not used
531                            if groupname not in changed[entry.Name]["ingroups"] :
532                                group = self.storage.getGroup(groupname)
533                                if group.Exists :
534                                    self.storage.addUserToGroup(entry, group)
535                                    changed[entry.Name]["ingroups"].append(groupname)
536                                else :
537                                    self.printInfo(_("Group %s not found in the PyKota Storage.") % groupname)
538                       
539        # Now outputs the list of nonexistent users and groups               
540        for name in missingusers.keys() :
541            self.printInfo(_("Nonexistent user %s") % name, level="warn")
542        for name in missinggroups.keys() :
543            self.printInfo(_("Nonexistent group %s") % name, level="warn")
544       
545        # Now delete what has to be deleted               
546        for (name, entry) in todelete.items() :               
547            entry.delete()
548                     
549if __name__ == "__main__" : 
550    retcode = 0
551    try :
552        defaults = { \
553                     "printer" : "*", \
554                     "comment" : "", \
555                   }
556        short_options = "vhdo:c:C:l:b:i:naugrp:P:S:H:G:RU:I:"
557        long_options = ["help", "version", "comment=", \
558                        "overcharge=", "charge=", "delete", "limitby=", \
559                        "balance=", "ingroups=", "noquota", "add", "users", \
560                        "groups", "reset", "hardreset", "prototype=", \
561                        "printer=", "softlimit=", "hardlimit=", "pgroups=", \
562                        "increase=", "used="]
563       
564        # Initializes the command line tool
565        editor = EdPyKota(doc=__doc__)
566        editor.deferredInit()
567       
568        # parse and checks the command line
569        (options, args) = editor.parseCommandline(sys.argv[1:], short_options, long_options)
570       
571        # sets long options
572        options["help"] = options["h"] or options["help"]
573        options["version"] = options["v"] or options["version"]
574        options["add"] = options["a"] or options["add"]
575        options["users"] = options["u"] or options["users"]
576        options["groups"] = options["g"] or options["groups"]
577        options["prototype"] = options["p"] or options["prototype"]
578        options["printer"] = options["P"] or options["printer"] or defaults["printer"]
579        options["softlimit"] = options["S"] or options["softlimit"]
580        options["hardlimit"] = options["H"] or options["hardlimit"] 
581        options["reset"] = options["r"] or options["reset"] 
582        options["noquota"] = options["n"] or options["noquota"]
583        options["limitby"] = options["l"] or options["limitby"]
584        options["balance"] = options["b"] or options["balance"] 
585        options["delete"] = options["d"] or options["delete"] 
586        options["ingroups"] = options["i"] or options["ingroups"]
587        options["charge"] = options["c"] or options["charge"]
588        options["pgroups"] = options["G"] or options["pgroups"]
589        options["hardreset"] = options["R"] or options["hardreset"] 
590        options["used"] = options["U"] or options["used"]
591        options["overcharge"] = options["o"] or options["overcharge"]
592        options["comment"] = options["C"] or options["comment"] or defaults["comment"]
593        options["increase"] = options["I"] or options["increase"]
594       
595        if options["help"] :
596            editor.display_usage_and_quit()
597        elif options["version"] :
598            editor.display_version_and_quit()
599        elif options["users"] and options["groups"] :   
600            raise PyKotaToolError, _("incompatible options, see help.")
601        elif (options["add"] or options["prototype"]) and options["delete"] :   
602            raise PyKotaToolError, _("incompatible options, see help.")
603        elif (options["reset"] or options["hardreset"] or options["limitby"] or options["used"] or options["balance"] or options["overcharge"] or options["softlimit"] or options["hardlimit"]) and options["prototype"] :
604            raise PyKotaToolError, _("incompatible options, see help.")
605        elif options["noquota"] and (options["prototype"] or options["hardlimit"] or options["softlimit"]) :
606            raise PyKotaToolError, _("incompatible options, see help.")
607        elif options["groups"] and (options["balance"] or options["ingroups"] or options["used"] or options["overcharge"]) :
608            raise PyKotaToolError, _("incompatible options, see help.")
609        elif options["comment"] and not options["balance"] :   
610            raise PyKotaToolError, _("incompatible options, see help.")
611        else :
612            retcode = editor.main(args, options)
613    except KeyboardInterrupt :       
614        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
615    except SystemExit :       
616        pass
617    except :
618        try :
619            editor.crashed("edpykota failed")
620        except :   
621            crashed("edpykota failed")
622        retcode = -1
623
624    try :
625        editor.storage.close()
626    except (TypeError, NameError, AttributeError) :   
627        pass
628       
629    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.