~ubuntu-branches/ubuntu/quantal/enigmail/quantal-security

« back to all changes in this revision

Viewing changes to build/unix/build-clang/tooltool.py

  • Committer: Package Import Robot
  • Author(s): Chris Coulson
  • Date: 2013-09-13 16:02:15 UTC
  • mfrom: (0.12.16)
  • Revision ID: package-import@ubuntu.com-20130913160215-u3g8nmwa0pdwagwc
Tags: 2:1.5.2-0ubuntu0.12.10.1
* New upstream release v1.5.2 for Thunderbird 24

* Build enigmail using a stripped down Thunderbird 17 build system, as it's
  now quite difficult to build the way we were doing previously, with the
  latest Firefox build system
* Add debian/patches/no_libxpcom.patch - Don't link against libxpcom, as it
  doesn't exist anymore (but exists in the build system)
* Add debian/patches/use_sdk.patch - Use the SDK version of xpt.py and
  friends
* Drop debian/patches/ipc-pipe_rename.diff (not needed anymore)
* Drop debian/patches/makefile_depth.diff (not needed anymore)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
 
3
 
#tooltool is a lookaside cache implemented in Python
4
 
#Copyright (C) 2011 John H. Ford <john@johnford.info>
5
 
#
6
 
#This program is free software; you can redistribute it and/or
7
 
#modify it under the terms of the GNU General Public License
8
 
#as published by the Free Software Foundation version 2
9
 
#
10
 
#This program is distributed in the hope that it will be useful,
11
 
#but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 
#GNU General Public License for more details.
14
 
#
15
 
#You should have received a copy of the GNU General Public License
16
 
#along with this program; if not, write to the Free Software
17
 
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18
 
 
19
 
# An manifest file specifies files in that directory that are stored
20
 
# elsewhere.  This file should only contain file in the directory
21
 
# which the manifest file resides in and it should be called 'manifest.manifest'
22
 
 
23
 
__version__ = '1'
24
 
 
25
 
import os
26
 
import optparse
27
 
import logging
28
 
import hashlib
29
 
import urllib2
30
 
import ConfigParser
31
 
try:
32
 
    import simplejson as json # I hear simplejson is faster
33
 
except ImportError:
34
 
    import json
35
 
 
36
 
log = logging.getLogger(__name__)
37
 
 
38
 
class FileRecordJSONEncoderException(Exception): pass
39
 
class InvalidManifest(Exception): pass
40
 
class ExceptionWithFilename(Exception):
41
 
    def __init__(self, filename):
42
 
        Exception.__init__(self)
43
 
        self.filename = filename
44
 
 
45
 
class DigestMismatchException(ExceptionWithFilename): pass
46
 
class MissingFileException(ExceptionWithFilename): pass
47
 
 
48
 
class FileRecord(object):
49
 
    def __init__(self, filename, size, digest, algorithm):
50
 
        object.__init__(self)
51
 
        self.filename = filename
52
 
        self.size = size
53
 
        self.digest = digest
54
 
        self.algorithm = algorithm
55
 
        log.debug("creating %s 0x%x" % (self.__class__.__name__, id(self)))
56
 
 
57
 
    def __eq__(self, other):
58
 
        if self is other:
59
 
            return True
60
 
        if self.filename == other.filename and \
61
 
            self.size == other.size and \
62
 
            self.digest == other.digest and \
63
 
            self.algorithm == other.algorithm:
64
 
            return True
65
 
        else:
66
 
            return False
67
 
 
68
 
    def __ne__(self, other):
69
 
        return not self.__eq__(other)
70
 
 
71
 
    def __str__(self):
72
 
        return repr(self)
73
 
 
74
 
    def __repr__(self):
75
 
        return "%s.%s(filename='%s', size='%s', digest='%s', algorithm='%s')" % (__name__,
76
 
                self.__class__.__name__,
77
 
                self.filename, self.size, self.digest, self.algorithm)
78
 
 
79
 
    def present(self):
80
 
        # Doesn't check validity
81
 
        return os.path.exists(self.filename)
82
 
 
83
 
    def validate_size(self):
84
 
        if self.present():
85
 
            return self.size == os.path.getsize(self.filename)
86
 
        else:
87
 
            log.debug("trying to validate size on a missing file, %s", self.filename)
88
 
            raise MissingFileException(filename=self.filename)
89
 
 
90
 
    def validate_digest(self):
91
 
        if self.present():
92
 
            with open(self.filename, 'rb') as f:
93
 
                return self.digest == digest_file(f, self.algorithm)
94
 
        else:
95
 
            log.debug("trying to validate digest on a missing file, %s', self.filename")
96
 
            raise MissingFileException(filename=self.filename)
97
 
 
98
 
    def validate(self):
99
 
        if self.validate_size():
100
 
            if self.validate_digest():
101
 
                return True
102
 
        return False
103
 
 
104
 
    def describe(self):
105
 
        if self.present() and self.validate():
106
 
            return "'%s' is present and valid" % self.filename
107
 
        elif self.present():
108
 
            return "'%s' is present and invalid" % self.filename
109
 
        else:
110
 
            return "'%s' is absent" % self.filename
111
 
 
112
 
 
113
 
def create_file_record(filename, algorithm):
114
 
    fo = open(filename, 'rb')
115
 
    stored_filename = os.path.split(filename)[1]
116
 
    fr = FileRecord(stored_filename, os.path.getsize(filename), digest_file(fo, algorithm), algorithm)
117
 
    fo.close()
118
 
    return fr
119
 
 
120
 
 
121
 
class FileRecordJSONEncoder(json.JSONEncoder):
122
 
    def encode_file_record(self, obj):
123
 
        if not issubclass(type(obj), FileRecord):
124
 
            err = "FileRecordJSONEncoder is only for FileRecord and lists of FileRecords, not %s" % obj.__class__.__name__
125
 
            log.warn(err)
126
 
            raise FileRecordJSONEncoderException(err)
127
 
        else:
128
 
            return {'filename': obj.filename, 'size': obj.size, 'algorithm': obj.algorithm, 'digest': obj.digest}
129
 
 
130
 
    def default(self, f):
131
 
        if issubclass(type(f), list):
132
 
            record_list = []
133
 
            for i in f:
134
 
                record_list.append(self.encode_file_record(i))
135
 
            return record_list
136
 
        else:
137
 
            return self.encode_file_record(f)
138
 
 
139
 
 
140
 
class FileRecordJSONDecoder(json.JSONDecoder):
141
 
    """I help the json module materialize a FileRecord from
142
 
    a JSON file.  I understand FileRecords and lists of
143
 
    FileRecords.  I ignore things that I don't expect for now"""
144
 
    # TODO: make this more explicit in what it's looking for
145
 
    # and error out on unexpected things
146
 
    def process_file_records(self, obj):
147
 
        if isinstance(obj, list):
148
 
            record_list = []
149
 
            for i in obj:
150
 
                record = self.process_file_records(i)
151
 
                if issubclass(type(record), FileRecord):
152
 
                    record_list.append(record)
153
 
            return record_list
154
 
        if isinstance(obj, dict) and \
155
 
           len(obj.keys()) == 4 and \
156
 
           obj.has_key('filename') and \
157
 
           obj.has_key('size') and \
158
 
           obj.has_key('algorithm') and \
159
 
           obj.has_key('digest'):
160
 
            rv = FileRecord(obj['filename'], obj['size'], obj['digest'], obj['algorithm'])
161
 
            log.debug("materialized %s" % rv)
162
 
            return rv
163
 
        return obj
164
 
 
165
 
    def decode(self, s):
166
 
        decoded = json.JSONDecoder.decode(self, s)
167
 
        rv = self.process_file_records(decoded)
168
 
        return rv
169
 
 
170
 
 
171
 
class Manifest(object):
172
 
 
173
 
    valid_formats = ('json',)
174
 
 
175
 
    def __init__(self, file_records=[]):
176
 
        self.file_records = file_records
177
 
 
178
 
    def __eq__(self, other):
179
 
        if self is other:
180
 
            return True
181
 
        if len(self.file_records) != len(other.file_records):
182
 
            log.debug('Manifests differ in number of files')
183
 
            return False
184
 
        #TODO: Lists in a different order should be equal
185
 
        for record in range(0,len(self.file_records)):
186
 
            if self.file_records[record] != other.file_records[record]:
187
 
                log.debug('FileRecords differ, %s vs %s' % (self.file_records[record],
188
 
                                                            other.file_records[record]))
189
 
                return False
190
 
        return True
191
 
 
192
 
    def __deepcopy__(self, memo):
193
 
        # This is required for a deep copy
194
 
        return Manifest(self.file_records[:])
195
 
 
196
 
    def __copy__(self):
197
 
        return Manifest(self.file_records)
198
 
 
199
 
    def copy(self):
200
 
        return Manifest(self.file_records[:])
201
 
 
202
 
    def present(self):
203
 
        return all(i.present() for i in self.file_records)
204
 
 
205
 
    def validate_sizes(self):
206
 
        return all(i.validate_size() for i in self.file_records)
207
 
 
208
 
    def validate_digests(self):
209
 
        return all(i.validate_digest() for i in self.file_records)
210
 
 
211
 
    def validate(self):
212
 
        return all(i.validate() for i in self.file_records)
213
 
 
214
 
    def sort(self):
215
 
        #TODO: WRITE TESTS
216
 
        self.file_records.sort(key=lambda x: x.size)
217
 
 
218
 
    def load(self, data_file, fmt='json'):
219
 
        assert fmt in self.valid_formats
220
 
        if fmt == 'json':
221
 
            try:
222
 
                self.file_records.extend(json.load(data_file, cls=FileRecordJSONDecoder))
223
 
                self.sort()
224
 
            except ValueError:
225
 
                raise InvalidManifest("trying to read invalid manifest file")
226
 
 
227
 
    def loads(self, data_string, fmt='json'):
228
 
        assert fmt in self.valid_formats
229
 
        if fmt == 'json':
230
 
            try:
231
 
                self.file_records.extend(json.loads(data_string, cls=FileRecordJSONDecoder))
232
 
                self.sort()
233
 
            except ValueError:
234
 
                raise InvalidManifest("trying to read invalid manifest file")
235
 
 
236
 
    def dump(self, output_file, fmt='json'):
237
 
        assert fmt in self.valid_formats
238
 
        self.sort()
239
 
        if fmt == 'json':
240
 
            rv = json.dump(self.file_records, output_file, indent=0, cls=FileRecordJSONEncoder)
241
 
            print >> output_file, ''
242
 
            return rv
243
 
 
244
 
    def dumps(self, fmt='json'):
245
 
        assert fmt in self.valid_formats
246
 
        self.sort()
247
 
        if fmt == 'json':
248
 
            return json.dumps(self.file_records, cls=FileRecordJSONEncoder)
249
 
 
250
 
 
251
 
def digest_file(f, a):
252
 
    """I take a file like object 'f' and return a hex-string containing
253
 
    of the result of the algorithm 'a' applied to 'f'."""
254
 
    h = hashlib.new(a)
255
 
    chunk_size = 1024*10
256
 
    data = f.read(chunk_size)
257
 
    while data:
258
 
        h.update(data)
259
 
        data = f.read(chunk_size)
260
 
    if hasattr(f, 'name'):
261
 
        log.debug('hashed %s with %s to be %s', f.name, a, h.hexdigest())
262
 
    else:
263
 
        log.debug('hashed a file with %s to be %s', a, h.hexdigest())
264
 
    return h.hexdigest()
265
 
 
266
 
# TODO: write tests for this function
267
 
def open_manifest(manifest_file):
268
 
    """I know how to take a filename and load it into a Manifest object"""
269
 
    if os.path.exists(manifest_file):
270
 
        manifest = Manifest()
271
 
        with open(manifest_file) as f:
272
 
            manifest.load(f)
273
 
            log.debug("loaded manifest from file '%s'" % manifest_file)
274
 
        return manifest
275
 
    else:
276
 
        log.debug("tried to load absent file '%s' as manifest" % manifest_file)
277
 
        raise InvalidManifest("manifest file '%s' does not exist" % manifest_file)
278
 
 
279
 
# TODO: write tests for this function
280
 
def list_manifest(manifest_file):
281
 
    """I know how print all the files in a location"""
282
 
    try:
283
 
        manifest = open_manifest(manifest_file)
284
 
    except InvalidManifest:
285
 
        log.error("failed to load manifest file at '%s'" % manifest_file)
286
 
        return False
287
 
    for f in manifest.file_records:
288
 
        print "%s\t%s\t%s" % ("P" if f.present() else "-",
289
 
                              "V" if f.present() and f.validate() else "-",
290
 
                              f.filename)
291
 
    return True
292
 
 
293
 
def validate_manifest(manifest_file):
294
 
    """I validate that all files in a manifest are present and valid but
295
 
    don't fetch or delete them if they aren't"""
296
 
    try:
297
 
        manifest = open_manifest(manifest_file)
298
 
    except InvalidManifest:
299
 
        log.error("failed to load manifest file at '%s'" % manifest_file)
300
 
        return False
301
 
    invalid_files = []
302
 
    absent_files = []
303
 
    for f in manifest.file_records:
304
 
        if not f.present():
305
 
            absent_files.append(f)
306
 
        else:
307
 
            if not f.validate():
308
 
                invalid_files.append(f)
309
 
    if len(invalid_files + absent_files) == 0:
310
 
        return True
311
 
    else:
312
 
        return False
313
 
 
314
 
# TODO: write tests for this function
315
 
def add_files(manifest_file, algorithm, filenames):
316
 
    # returns True if all files successfully added, False if not
317
 
    # and doesn't catch library Exceptions.  If any files are already
318
 
    # tracked in the manifest, return will be False because they weren't
319
 
    # added
320
 
    all_files_added = True
321
 
    # Create a old_manifest object to add to
322
 
    if os.path.exists(manifest_file):
323
 
        old_manifest = open_manifest(manifest_file)
324
 
    else:
325
 
        old_manifest = Manifest()
326
 
        log.debug("creating a new manifest file")
327
 
    new_manifest = Manifest() # use a different manifest for the output
328
 
    for filename in filenames:
329
 
        log.debug("adding %s" % filename)
330
 
        path, name = os.path.split(filename)
331
 
        new_fr = create_file_record(filename, algorithm)
332
 
        log.debug("appending a new file record to manifest file")
333
 
        add = True
334
 
        for fr in old_manifest.file_records:
335
 
            log.debug("manifest file has '%s'" % "', ".join([x.filename for x in old_manifest.file_records]))
336
 
            if new_fr == fr and new_fr.validate():
337
 
                # TODO: Decide if this case should really cause a False return
338
 
                log.info("file already in old_manifest file and matches")
339
 
                add = False
340
 
            elif new_fr == fr and not new_fr.validate():
341
 
                log.error("file already in old_manifest file but is invalid")
342
 
                add = False
343
 
            if filename == fr.filename:
344
 
                log.error("manifest already contains file named %s" % filename)
345
 
                add = False
346
 
        if add:
347
 
            new_manifest.file_records.append(new_fr)
348
 
            log.debug("added '%s' to manifest" % filename)
349
 
        else:
350
 
            all_files_added = False
351
 
    with open(manifest_file, 'wb') as output:
352
 
        new_manifest.dump(output, fmt='json')
353
 
    return all_files_added
354
 
 
355
 
 
356
 
# TODO: write tests for this function
357
 
def fetch_file(base_url, file_record, overwrite=False, grabchunk=1024*4):
358
 
    # A file which is requested to be fetched that exists locally will be hashed.
359
 
    # If the hash matches the requested file's hash, nothing will be done and the
360
 
    # function will return.  If the function is told to overwrite and there is a 
361
 
    # digest mismatch, the exiting file will be overwritten
362
 
    if file_record.present():
363
 
        if file_record.validate():
364
 
            log.info("existing '%s' is valid, not fetching" % file_record.filename)
365
 
            return True
366
 
        if overwrite:
367
 
            log.info("overwriting '%s' as requested" % file_record.filename)
368
 
        else:
369
 
            # All of the following is for a useful error message
370
 
            with open(file_record.filename, 'rb') as f:
371
 
                d = digest_file(f, file_record.algorithm)
372
 
            log.error("digest mismatch between manifest(%s...) and local file(%s...)" % \
373
 
                    (file_record.digest[:8], d[:8]))
374
 
            log.debug("full digests: manifest (%s) local file (%s)" % (file_record.digest, d))
375
 
            # Let's bail!
376
 
            return False
377
 
 
378
 
    # Generate the URL for the file on the server side
379
 
    url = "%s/%s/%s" % (base_url, file_record.algorithm, file_record.digest)
380
 
 
381
 
    log.debug("fetching from '%s'" % url)
382
 
 
383
 
    # TODO: This should be abstracted to make generic retreival protocol handling easy
384
 
    # Well, the file doesn't exist locally.  Lets fetch it.
385
 
    try:
386
 
        f = urllib2.urlopen(url)
387
 
        log.debug("opened %s for reading" % url)
388
 
        with open(file_record.filename, 'wb') as out:
389
 
            k = True
390
 
            size = 0
391
 
            while k:
392
 
                # TODO: print statistics as file transfers happen both for info and to stop
393
 
                # buildbot timeouts
394
 
                indata = f.read(grabchunk)
395
 
                out.write(indata)
396
 
                size += len(indata)
397
 
                if indata == '':
398
 
                    k = False
399
 
            if size != file_record.size:
400
 
                log.error("transfer from %s to %s failed due to a difference of %d bytes" % (url,
401
 
                            file_record.filename, file_record.size - size))
402
 
                return False
403
 
            log.info("fetched %s" % file_record.filename)
404
 
    except (urllib2.URLError, urllib2.HTTPError) as e:
405
 
        log.error("failed to fetch '%s': %s" % (file_record.filename, e),
406
 
                  exc_info=True)
407
 
        return False
408
 
    except IOError:
409
 
        log.error("failed to write to '%s'" % file_record.filename,
410
 
                  exc_info=True)
411
 
        return False
412
 
    return True
413
 
 
414
 
 
415
 
# TODO: write tests for this function
416
 
def fetch_files(manifest_file, base_url, overwrite, filenames=[]):
417
 
    # Lets load the manifest file
418
 
    try:
419
 
        manifest = open_manifest(manifest_file)
420
 
    except InvalidManifest:
421
 
        log.error("failed to load manifest file at '%s'" % manifest_file)
422
 
        return False
423
 
    # We want to track files that fail to be fetched as well as
424
 
    # files that are fetched
425
 
    failed_files = []
426
 
 
427
 
    # Lets go through the manifest and fetch the files that we want
428
 
    fetched_files = []
429
 
    for f in manifest.file_records:
430
 
        if f.filename in filenames or len(filenames) == 0:
431
 
            log.debug("fetching %s" % f.filename)
432
 
            if fetch_file(base_url, f, overwrite):
433
 
                fetched_files.append(f)
434
 
            else:
435
 
                failed_files.append(f.filename)
436
 
        else:
437
 
            log.debug("skipping %s" % f.filename)
438
 
 
439
 
    # Even if we get the file, lets ensure that it matches what the
440
 
    # manifest specified
441
 
    for localfile in fetched_files:
442
 
        if not localfile.validate():
443
 
            log.error("'%s'" % localfile.describe())
444
 
 
445
 
    # If we failed to fetch or validate a file, we need to fail
446
 
    if len(failed_files) > 0:
447
 
        log.error("The following files failed: '%s'" % "', ".join(failed_files))
448
 
        return False
449
 
    return True
450
 
 
451
 
 
452
 
# TODO: write tests for this function
453
 
def process_command(options, args):
454
 
    """ I know how to take a list of program arguments and
455
 
    start doing the right thing with them"""
456
 
    cmd = args[0]
457
 
    cmd_args = args[1:]
458
 
    log.debug("processing '%s' command with args '%s'" % (cmd, '", "'.join(cmd_args)))
459
 
    log.debug("using options: %s" % options)
460
 
    if cmd == 'list':
461
 
        return list_manifest(options['manifest'])
462
 
    if cmd == 'validate':
463
 
        return validate_manifest(options['manifest'])
464
 
    elif cmd == 'add':
465
 
        return add_files(options['manifest'], options['algorithm'], cmd_args)
466
 
    elif cmd == 'fetch':
467
 
        if not options.has_key('base_url') or options.get('base_url') is None:
468
 
            log.critical('fetch command requires url option')
469
 
            return False
470
 
        return fetch_files(options['manifest'], options['base_url'], options['overwrite'], cmd_args)
471
 
    else:
472
 
        log.critical('command "%s" is not implemented' % cmd)
473
 
        return False
474
 
 
475
 
# fetching api:
476
 
#   http://hostname/algorithm/hash
477
 
#   example: http://people.mozilla.org/sha1/1234567890abcedf
478
 
# This will make it possible to have the server allow clients to
479
 
# use different algorithms than what was uploaded to the server
480
 
 
481
 
# TODO: Implement the following features:
482
 
#   -optimization: do small files first, justification is that they are faster
483
 
#    and cause a faster failure if they are invalid
484
 
#   -store permissions
485
 
#   -local renames i.e. call the file one thing on the server and
486
 
#    something different locally
487
 
#   -deal with the cases:
488
 
#     -local data matches file requested with different filename
489
 
#     -two different files with same name, different hash
490
 
#   -?only ever locally to digest as filename, symlink to real name
491
 
#   -?maybe deal with files as a dir of the filename with all files in that dir as the versions of that file
492
 
#      - e.g. ./python-2.6.7.dmg/0123456789abcdef and ./python-2.6.7.dmg/abcdef0123456789
493
 
 
494
 
def main():
495
 
    # Set up logging, for now just to the console
496
 
    ch = logging.StreamHandler()
497
 
    cf = logging.Formatter("%(levelname)s - %(message)s")
498
 
    ch.setFormatter(cf)
499
 
 
500
 
    # Set up option parsing
501
 
    parser = optparse.OptionParser()
502
 
    # I wish there was a way to say "only allow args to be
503
 
    # sequential and at the end of the argv.
504
 
    # OH! i could step through sys.argv and check for things starting without -/-- before things starting with them
505
 
    parser.add_option('-q', '--quiet', default=False,
506
 
            dest='quiet', action='store_true')
507
 
    parser.add_option('-v', '--verbose', default=False,
508
 
            dest='verbose', action='store_true')
509
 
    parser.add_option('-m', '--manifest', default='manifest.tt',
510
 
            dest='manifest', action='store',
511
 
            help='specify the manifest file to be operated on')
512
 
    parser.add_option('-d', '--algorithm', default='sha512',
513
 
            dest='algorithm', action='store',
514
 
            help='openssl hashing algorithm to use')
515
 
    parser.add_option('-o', '--overwrite', default=False,
516
 
            dest='overwrite', action='store_true',
517
 
            help='if fetching, remote copy will overwrite a local copy that is different. ')
518
 
    parser.add_option('--url', dest='base_url', action='store',
519
 
            help='base url for fetching files')
520
 
    parser.add_option('--ignore-config-files', action='store_true', default=False,
521
 
                     dest='ignore_cfg_files')
522
 
    (options_obj, args) = parser.parse_args()
523
 
    # Dictionaries are easier to work with
524
 
    options = vars(options_obj)
525
 
 
526
 
 
527
 
    # Use some of the option parser to figure out application
528
 
    # log level
529
 
    if options.get('verbose'):
530
 
        ch.setLevel(logging.DEBUG)
531
 
    elif options.get('quiet'):
532
 
        ch.setLevel(logging.ERROR)
533
 
    else:
534
 
        ch.setLevel(logging.INFO)
535
 
    log.addHandler(ch)
536
 
 
537
 
    cfg_file = ConfigParser.SafeConfigParser()
538
 
    if not options.get("ignore_cfg_files"):
539
 
        read_files = cfg_file.read(['/etc/tooltool', os.path.expanduser('~/.tooltool'),
540
 
                   os.path.join(os.getcwd(), '.tooltool')])
541
 
        log.debug("read in the config files '%s'" % '", '.join(read_files))
542
 
    else:
543
 
        log.debug("skipping config files")
544
 
 
545
 
    for option in ('base_url', 'algorithm'):
546
 
        if not options.get(option):
547
 
            try:
548
 
                options[option] = cfg_file.get('general', option)
549
 
                log.debug("read '%s' as '%s' from cfg_file" % (option, options[option]))
550
 
            except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) as e:
551
 
                log.debug("%s in config file" % e, exc_info=True)
552
 
 
553
 
    if not options.has_key('manifest'):
554
 
        parser.error("no manifest file specified")
555
 
 
556
 
    if len(args) < 1:
557
 
        parser.error('You must specify a command')
558
 
    exit(0 if process_command(options, args) else 1)
559
 
 
560
 
if __name__ == "__main__":
561
 
    main()
562
 
else:
563
 
    log.addHandler(logging.NullHandler())
564
 
    #log.addHandler(logging.StreamHandler())