~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to scripts/ftpmaster-tools/sync-source.py

Undo rename. Again.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python -S
2
2
#
3
 
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 
3
# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
4
4
# GNU Affero General Public License version 3 (see the file LICENSE).
5
5
 
6
6
# <james.troup@canonical.com>
14
14
will become a matter of simply 'publishing' source from Debian unstable
15
15
wherever) into Ubuntu dapper and the whole fake upload trick can go away.
16
16
"""
17
 
 
 
17
import commands
18
18
import errno
 
19
import optparse
19
20
import os
20
21
import re
21
22
import shutil
27
28
import _pythonpath
28
29
from _syncorigins import origins
29
30
import apt_pkg
 
31
from contrib.glock import GlobalLock
 
32
import dak_utils
30
33
from debian.deb822 import Dsc
31
34
from zope.component import getUtility
32
35
 
34
37
    cursor,
35
38
    sqlvalues,
36
39
    )
 
40
from canonical.launchpad.scripts import (
 
41
    execute_zcml_for_scripts,
 
42
    logger,
 
43
    logger_options,
 
44
    )
37
45
from canonical.librarian.client import LibrarianClient
 
46
from canonical.lp import initZopeless
38
47
from lp.archiveuploader.utils import (
39
48
    DpkgSourceError,
40
49
    extract_dpkg_source,
42
51
from lp.registry.interfaces.distribution import IDistributionSet
43
52
from lp.registry.interfaces.person import IPersonSet
44
53
from lp.registry.interfaces.pocket import PackagePublishingPocket
45
 
from lp.services.scripts.base import (
46
 
    LaunchpadScript,
47
 
    LaunchpadScriptFailure,
48
 
    )
49
 
from lp.soyuz.enums import (
50
 
    PackagePublishingStatus,
51
 
    re_bug_numbers,
52
 
    re_closes,
53
 
    re_lp_closes,
54
 
    )
 
54
from lp.soyuz.enums import PackagePublishingStatus
55
55
from lp.soyuz.scripts.ftpmaster import (
56
56
    generate_changes,
57
57
    SyncSource,
60
60
 
61
61
 
62
62
reject_message = ""
63
 
re_no_epoch = re.compile(r"^\d+\:")
64
63
re_strip_revision = re.compile(r"-([^-]+)$")
65
64
re_changelog_header = re.compile(
66
65
    r"^\S+ \((?P<version>.*)\) .*;.*urgency=(?P<urgency>\w+).*")
 
66
re_closes = re.compile(
 
67
    r"closes:\s*(?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*", re.I)
 
68
re_lp_closes = re.compile(r"lp:\s+\#\d+(?:,\s*\#\d+)*", re.I)
 
69
re_bug_numbers = re.compile(r"\#?\s?(\d+)")
67
70
 
68
71
 
69
72
Blacklisted = None
70
73
Library = None
 
74
Lock = None
71
75
Log = None
72
76
Options = None
73
77
 
107
111
    return urgency_map.get(n, 'low')
108
112
 
109
113
 
 
114
def sign_changes(changes, dsc):
 
115
    # XXX cprov 2007-07-06: hardcoded file locations and parameters for
 
116
    # production.
 
117
    temp_filename = "unsigned-changes"
 
118
    keyid = "0C12BDD7"
 
119
    secret_keyring = "/srv/launchpad.net/dot-gnupg/secring.gpg"
 
120
    pub_keyring = "/srv/launchpad.net/dot-gnupg/pubring.gpg"
 
121
 
 
122
    filehandle = open(temp_filename, 'w')
 
123
    filehandle.write(changes)
 
124
    filehandle.close()
 
125
 
 
126
    output_filename = "%s_%s_source.changes" % (
 
127
        dsc["source"], dak_utils.re_no_epoch.sub('', dsc["version"]))
 
128
 
 
129
    cmd = ("gpg --no-options --batch --no-tty --secret-keyring=%s "
 
130
           "--keyring=%s --default-key=0x%s --output=%s --clearsign %s" %
 
131
           (secret_keyring, pub_keyring, keyid, output_filename,
 
132
            temp_filename))
 
133
    result, output = commands.getstatusoutput(cmd)
 
134
 
 
135
    if (result != 0):
 
136
        print " * command was '%s'" % (cmd)
 
137
        print (dak_utils.prefix_multi_line_string(
 
138
                output, " [gpg output:] "), "")
 
139
        dak_utils.fubar("%s: signing .changes failed [return code: %s]." %
 
140
                        (output_filename, result))
 
141
 
 
142
    os.unlink(temp_filename)
 
143
 
 
144
 
110
145
def parse_changelog(changelog_filename, previous_version):
111
146
    if not os.path.exists(changelog_filename):
112
 
        raise LaunchpadScriptFailure(
113
 
            "debian/changelog not found in extracted source.")
 
147
        dak_utils.fubar("debian/changelog not found in extracted source.")
114
148
    urgency = urgency_to_numeric('low')
115
149
    changes = ""
116
150
    is_debian_changelog = 0
130
164
        changes += line
131
165
 
132
166
    if not is_debian_changelog:
133
 
        raise LaunchpadScriptFailure("header not found in debian/changelog")
 
167
        dak_utils.fubar("header not found in debian/changelog")
134
168
 
135
169
    closes = []
136
170
    for match in re_closes.finditer(changes):
189
223
    source_description = ""
190
224
 
191
225
    if not os.path.exists(control_filename):
192
 
        raise LaunchpadScriptFailure(
193
 
            "debian/control not found in extracted source.")
 
226
        dak_utils.fubar("debian/control not found in extracted source.")
194
227
    control_filehandle = open(control_filename)
195
228
    Control = apt_pkg.ParseTagFile(control_filehandle)
196
229
    while Control.Step():
224
257
    except DpkgSourceError, e:
225
258
        print " * command was '%s'" % (e.command)
226
259
        print e.output
227
 
        raise LaunchpadScriptFailure(
 
260
        dak_utils.fubar(
228
261
            "'dpkg-source -x' failed for %s [return code: %s]." %
229
262
            (dsc_filename, e.result))
230
263
 
236
269
    # Sanity check that'll probably break if people set $TMPDIR, but
237
270
    # WTH, shutil.rmtree scares me
238
271
    if not tmpdir.startswith("/tmp/"):
239
 
        raise LaunchpadScriptFailure(
240
 
            "%s: tmpdir doesn't start with /tmp" % (tmpdir))
 
272
        dak_utils.fubar("%s: tmpdir doesn't start with /tmp" % (tmpdir))
241
273
 
242
274
    # Move back and cleanup the temporary tree
243
275
    os.chdir(old_cwd)
245
277
        shutil.rmtree(tmpdir)
246
278
    except OSError, e:
247
279
        if errno.errorcode[e.errno] != 'EACCES':
248
 
            raise LaunchpadScriptFailure(
 
280
            dak_utils.fubar(
249
281
                "%s: couldn't remove tmp dir for source tree."
250
282
                % (dsc["source"]))
251
283
 
256
288
        cmd = "chmod -R u+rwx %s" % (tmpdir)
257
289
        result = os.system(cmd)
258
290
        if result != 0:
259
 
            raise LaunchpadScriptFailure(
260
 
                "'%s' failed with result %s." % (cmd, result))
 
291
            dak_utils.fubar("'%s' failed with result %s." % (cmd, result))
261
292
        shutil.rmtree(tmpdir)
262
293
    except:
263
 
        raise LaunchpadScriptFailure(
 
294
        dak_utils.fubar(
264
295
            "%s: couldn't remove tmp dir for source tree." % (dsc["source"]))
265
296
 
266
297
 
278
309
            # override a main binary package
279
310
            if current_component == "main" and source_component != "main":
280
311
                if not Options.forcemore:
281
 
                    raise LaunchpadScriptFailure(
 
312
                    dak_utils.fubar(
282
313
                        "%s is in main but its source (%s) is not." %
283
314
                        (binary, source))
284
315
                else:
285
 
                    Log.warning(
 
316
                    dak_utils.warn(
286
317
                        "%s is in main but its source (%s) is not - "
287
318
                        "continuing anyway." % (binary, source))
288
319
 
290
321
            # ubuntu-modified binary package
291
322
            ubuntu_bin = current_binaries[binary][0].find("ubuntu")
292
323
            if not Options.force and ubuntu_bin != -1:
293
 
                raise LaunchpadScriptFailure(
 
324
                dak_utils.fubar(
294
325
                    "%s is trying to override %s_%s without -f/--force." %
295
326
                    (source, binary, current_version))
296
327
            print "I: %s [%s] -> %s_%s [%s]." % (
308
339
        dsc_file.seek(0)
309
340
        (gpg_pre, payload, gpg_post) = Dsc.split_gpg_and_payload(dsc_file)
310
341
        if gpg_pre == [] and gpg_post == []:
311
 
            raise LaunchpadScriptFailure(
312
 
                "signature required for %s but not present" % dsc_filename)
 
342
            dak_utils.fubar("signature required for %s but not present"
 
343
                % dsc_filename)
313
344
        if signing_rules == "must be signed and valid":
314
345
            if (gpg_pre[0] != "-----BEGIN PGP SIGNED MESSAGE-----" or
315
346
                gpg_post[0] != "-----BEGIN PGP SIGNATURE-----"):
316
 
                raise LaunchpadScriptFailure(
317
 
                    "signature for %s invalid %r %r" %
318
 
                    (dsc_filename, gpg_pre, gpg_post))
 
347
                dak_utils.fubar("signature for %s invalid %r %r" % (
 
348
                    dsc_filename, gpg_pre, gpg_post))
319
349
 
320
350
    dsc_files = dict((entry['name'], entry) for entry in dsc['files'])
321
351
    check_dsc(dsc, current_sources, current_binaries)
329
359
    (old_cwd, tmpdir) = extract_source(dsc_filename)
330
360
 
331
361
    # Get the upstream version
332
 
    upstr_version = re_no_epoch.sub('', dsc["version"])
 
362
    upstr_version = dak_utils.re_no_epoch.sub('', dsc["version"])
333
363
    if re_strip_revision.search(upstr_version):
334
364
        upstr_version = re_strip_revision.sub('', upstr_version)
335
365
 
353
383
        section, priority, description, files_from_librarian, requested_by,
354
384
        origin)
355
385
 
 
386
    # XXX cprov 2007-07-03: Soyuz wants an unsigned changes
 
387
    #sign_changes(changes, dsc)
356
388
    output_filename = "%s_%s_source.changes" % (
357
 
        dsc["source"], re_no_epoch.sub('', dsc["version"]))
 
389
        dsc["source"], dak_utils.re_no_epoch.sub('', dsc["version"]))
358
390
 
359
391
    filehandle = open(output_filename, 'w')
360
392
    try:
390
422
 
391
423
        if (valid_component is not None and
392
424
            component != valid_component.name):
393
 
            Log.warning(
 
425
            dak_utils.warn(
394
426
                "%s/%s: skipping because it is not in %s component" % (
395
427
                pkg, version, component))
396
428
            continue
399
431
            S[pkg] = [version, component]
400
432
        else:
401
433
            if apt_pkg.VersionCompare(S[pkg][0], version) < 0:
402
 
                Log.warning(
 
434
                dak_utils.warn(
403
435
                    "%s: skipping because %s is < %s" % (
404
436
                    pkg, version, S[pkg][0]))
405
437
                S[pkg] = [version, component]
511
543
 
512
544
    # Check it's in the Sources file
513
545
    if pkg not in Sources:
514
 
        raise LaunchpadScriptFailure(
515
 
            "%s doesn't exist in the Sources file." % (pkg))
 
546
        dak_utils.fubar("%s doesn't exist in the Sources file." % (pkg))
516
547
 
517
548
    syncsource = SyncSource(Sources[pkg]["files"], origin, Log,
518
549
        urllib.urlretrieve, Options.todistro)
521
552
        dsc_filename = syncsource.fetchSyncFiles()
522
553
        syncsource.checkDownloadedFiles()
523
554
    except SyncSourceError, e:
524
 
        raise LaunchpadScriptFailure("Fetching files failed: %s" % (str(e),))
 
555
        dak_utils.fubar("Fetching files failed: %s" % (str(e),))
525
556
 
526
557
    if dsc_filename is None:
527
 
        raise LaunchpadScriptFailure(
 
558
        dak_utils.fubar(
528
559
            "No dsc filename in %r" % Sources[pkg]["files"].keys())
529
560
 
530
561
    import_dsc(os.path.abspath(dsc_filename), suite, previous_version,
532
563
               current_sources, current_binaries)
533
564
 
534
565
 
535
 
class Percentages:
536
 
    """Helper to compute percentage ratios compared to a fixed total."""
537
 
 
538
 
    def __init__(self, total):
539
 
        self.total = total
540
 
 
541
 
    def get_ratio(self, number):
542
 
        """Report the ration of `number` to `self.total`, as a percentage."""
543
 
        return (float(number) / self.total) * 100
544
 
 
545
 
 
546
566
def do_diff(Sources, Suite, origin, arguments, current_binaries):
547
567
    stat_us = 0
548
568
    stat_cant_update = 0
564
584
 
565
585
        if pkg not in Sources:
566
586
            if not Options.all:
567
 
                raise LaunchpadScriptFailure("%s: not found" % (pkg))
 
587
                dak_utils.fubar("%s: not found" % (pkg))
568
588
            else:
569
589
                print "[Ubuntu Specific] %s_%s" % (pkg, dest_version)
570
590
                stat_us += 1
607
627
                        % (pkg, dest_version, source_version))
608
628
 
609
629
    if Options.all:
610
 
        percentages = Percentages(stat_count)
611
630
        print
612
631
        print ("Out-of-date BUT modified: %3d (%.2f%%)"
613
 
            % (stat_cant_update, percentages.get_ratio(stat_cant_update)))
 
632
               % (stat_cant_update, (float(stat_cant_update)/stat_count)*100))
614
633
        print ("Updated:                  %3d (%.2f%%)"
615
 
            % (stat_updated, percentages.get_ratio(stat_updated)))
 
634
               % (stat_updated, (float(stat_updated)/stat_count)*100))
616
635
        print ("Ubuntu Specific:          %3d (%.2f%%)"
617
 
            % (stat_us, percentages.get_ratio(stat_us)))
 
636
               % (stat_us, (float(stat_us)/stat_count)*100))
618
637
        print ("Up-to-date [Modified]:    %3d (%.2f%%)"
619
 
            % (stat_uptodate_modified, percentages.get_ratio(
620
 
                stat_uptodate_modified)))
 
638
               % (stat_uptodate_modified,
 
639
                  (float(stat_uptodate_modified)/stat_count)*100))
621
640
        print ("Up-to-date:               %3d (%.2f%%)"
622
 
               % (stat_uptodate, percentages.get_ratio(stat_uptodate)))
 
641
               % (stat_uptodate, (float(stat_uptodate)/stat_count)*100))
623
642
        print ("Blacklisted:              %3d (%.2f%%)"
624
 
               % (stat_blacklisted, percentages.get_ratio(stat_blacklisted)))
 
643
               % (stat_blacklisted, (float(stat_blacklisted)/stat_count)*100))
625
644
        print ("Broken:                   %3d (%.2f%%)"
626
 
               % (stat_broken, percentages.get_ratio(stat_broken)))
 
645
               % (stat_broken, (float(stat_broken)/stat_count)*100))
627
646
        print "                          -----------"
628
647
        print "Total:                    %s" % (stat_count)
629
648
 
630
649
 
 
650
def options_setup():
 
651
    global Log, Options
 
652
 
 
653
    parser = optparse.OptionParser()
 
654
    logger_options(parser)
 
655
    parser.add_option("-a", "--all", dest="all",
 
656
                      default=False, action="store_true",
 
657
                      help="sync all packages")
 
658
    parser.add_option("-b", "--requested-by", dest="requestor",
 
659
                      help="who the sync was requested by")
 
660
    parser.add_option("-f", "--force", dest="force",
 
661
                      default=False, action="store_true",
 
662
                      help="force sync over the top of Ubuntu changes")
 
663
    parser.add_option("-F", "--force-more", dest="forcemore",
 
664
                      default=False, action="store_true",
 
665
                      help="force sync even when components don't match")
 
666
    parser.add_option("-n", "--noaction", dest="action",
 
667
                      default=True, action="store_false",
 
668
                      help="don't do anything")
 
669
 
 
670
    # XXX cprov 2007-07-03: Why the heck doesn't -v provide by logger provide
 
671
    # Options.verbose?
 
672
    parser.add_option("-V", "--moreverbose", dest="moreverbose",
 
673
                      default=False, action="store_true",
 
674
                      help="be even more verbose")
 
675
 
 
676
    # Options controlling where to sync packages to:
 
677
    parser.add_option("-c", "--to-component", dest="tocomponent",
 
678
                      help="limit syncs to packages in COMPONENT")
 
679
    parser.add_option("-d", "--to-distro", dest="todistro",
 
680
                      default='ubuntu', help="sync to DISTRO")
 
681
    parser.add_option("-s", "--to-suite", dest="tosuite",
 
682
                      help="sync to SUITE (aka distroseries)")
 
683
 
 
684
    # Options controlling where to sync packages from:
 
685
    parser.add_option("-C", "--from-component", dest="fromcomponent",
 
686
                      help="sync from COMPONENT")
 
687
    parser.add_option("-D", "--from-distro", dest="fromdistro",
 
688
                      default='debian', help="sync from DISTRO")
 
689
    parser.add_option("-S", "--from-suite", dest="fromsuite",
 
690
                      help="sync from SUITE (aka distroseries)")
 
691
    parser.add_option("-B", "--blacklist", dest="blacklist_path",
 
692
                      default="/srv/launchpad.net/dak/sync-blacklist.txt",
 
693
                      help="Blacklist file path.")
 
694
 
 
695
 
 
696
    (Options, arguments) = parser.parse_args()
 
697
 
 
698
    distro = Options.fromdistro.lower()
 
699
    if not Options.fromcomponent:
 
700
        Options.fromcomponent = origins[distro]["default component"]
 
701
    if not Options.fromsuite:
 
702
        Options.fromsuite = origins[distro]["default suite"]
 
703
 
 
704
    # Sanity checks on options
 
705
    if not Options.all and not arguments:
 
706
        dak_utils.fubar(
 
707
            "Need -a/--all or at least one package name as an argument.")
 
708
 
 
709
    return arguments
 
710
 
 
711
 
631
712
def objectize_options():
632
713
    """Parse given options.
633
714
 
647
728
    if Options.tocomponent is not None:
648
729
 
649
730
        if Options.tocomponent not in valid_components:
650
 
            raise LaunchpadScriptFailure(
 
731
            dak_utils.fubar(
651
732
                "%s is not a valid component for %s/%s."
652
733
                % (Options.tocomponent, Options.todistro.name,
653
734
                   Options.tosuite.name))
661
742
    PersonSet = getUtility(IPersonSet)
662
743
    person = PersonSet.getByName(Options.requestor)
663
744
    if not person:
664
 
        raise LaunchpadScriptFailure(
665
 
            "Unknown LaunchPad user id '%s'." % (Options.requestor))
 
745
        dak_utils.fubar("Unknown LaunchPad user id '%s'."
 
746
                        % (Options.requestor))
666
747
    Options.requestor = "%s <%s>" % (person.displayname,
667
748
                                     person.preferredemail.email)
668
749
    Options.requestor = Options.requestor.encode("ascii", "replace")
688
769
    try:
689
770
        blacklist_file = open(path)
690
771
    except IOError:
691
 
        Log.warning('Could not find blacklist file on %s' % path)
 
772
        dak_utils.warn('Could not find blacklist file on %s' % path)
692
773
        return blacklist
693
774
 
694
775
    for line in blacklist_file:
705
786
    return blacklist
706
787
 
707
788
 
708
 
class SyncSourceScript(LaunchpadScript):
709
 
 
710
 
    def add_my_options(self):
711
 
        self.parser.add_option("-a", "--all", dest="all",
712
 
                        default=False, action="store_true",
713
 
                        help="sync all packages")
714
 
        self.parser.add_option("-b", "--requested-by", dest="requestor",
715
 
                        help="who the sync was requested by")
716
 
        self.parser.add_option("-f", "--force", dest="force",
717
 
                        default=False, action="store_true",
718
 
                        help="force sync over the top of Ubuntu changes")
719
 
        self.parser.add_option("-F", "--force-more", dest="forcemore",
720
 
                        default=False, action="store_true",
721
 
                        help="force sync even when components don't match")
722
 
        self.parser.add_option("-n", "--noaction", dest="action",
723
 
                        default=True, action="store_false",
724
 
                        help="don't do anything")
725
 
 
726
 
        # Options controlling where to sync packages to:
727
 
        self.parser.add_option("-c", "--to-component", dest="tocomponent",
728
 
                        help="limit syncs to packages in COMPONENT")
729
 
        self.parser.add_option("-d", "--to-distro", dest="todistro",
730
 
                        default='ubuntu', help="sync to DISTRO")
731
 
        self.parser.add_option("-s", "--to-suite", dest="tosuite",
732
 
                        help="sync to SUITE (aka distroseries)")
733
 
 
734
 
        # Options controlling where to sync packages from:
735
 
        self.parser.add_option("-C", "--from-component", dest="fromcomponent",
736
 
                        help="sync from COMPONENT")
737
 
        self.parser.add_option("-D", "--from-distro", dest="fromdistro",
738
 
                        default='debian', help="sync from DISTRO")
739
 
        self.parser.add_option("-S", "--from-suite", dest="fromsuite",
740
 
                        help="sync from SUITE (aka distroseries)")
741
 
        self.parser.add_option("-B", "--blacklist", dest="blacklist_path",
742
 
                        default="/srv/launchpad.net/dak/sync-blacklist.txt",
743
 
                        help="Blacklist file path.")
744
 
 
745
 
    def main(self):
746
 
        global Blacklisted, Library, Log, Options
747
 
 
748
 
        Log = self.logger
749
 
        Options = self.options
750
 
 
751
 
        distro = Options.fromdistro.lower()
752
 
        if not Options.fromcomponent:
753
 
            Options.fromcomponent = origins[distro]["default component"]
754
 
        if not Options.fromsuite:
755
 
            Options.fromsuite = origins[distro]["default suite"]
756
 
 
757
 
        # Sanity checks on options
758
 
        if not Options.all and not self.args:
759
 
            raise LaunchpadScriptFailure(
760
 
                "Need -a/--all or at least one package name as an argument.")
761
 
 
762
 
        apt_pkg.init()
763
 
        Library = LibrarianClient()
764
 
 
765
 
        objectize_options()
766
 
 
767
 
        Blacklisted = parseBlacklist(Options.blacklist_path)
768
 
 
769
 
        origin = origins[Options.fromdistro]
770
 
        origin["suite"] = Options.fromsuite
771
 
        origin["component"] = Options.fromcomponent
772
 
 
773
 
        Sources = read_Sources("Sources", origin)
774
 
        Suite = read_current_source(
775
 
            Options.tosuite, Options.tocomponent, self.args)
776
 
        current_binaries = read_current_binaries(Options.tosuite)
777
 
        do_diff(Sources, Suite, origin, self.args, current_binaries)
 
789
def init():
 
790
    global Blacklisted, Library, Lock, Log, Options
 
791
 
 
792
    apt_pkg.init()
 
793
 
 
794
    arguments = options_setup()
 
795
 
 
796
    Log = logger(Options, "sync-source")
 
797
 
 
798
    Log.debug("Acquiring lock")
 
799
    Lock = GlobalLock('/var/lock/launchpad-sync-source.lock')
 
800
    Lock.acquire(blocking=True)
 
801
 
 
802
    Log.debug("Initializing connection.")
 
803
    execute_zcml_for_scripts()
 
804
    initZopeless(dbuser="ro")
 
805
 
 
806
    Library = LibrarianClient()
 
807
 
 
808
    objectize_options()
 
809
 
 
810
    Blacklisted = parseBlacklist(Options.blacklist_path)
 
811
 
 
812
    return arguments
 
813
 
 
814
 
 
815
def main():
 
816
    arguments = init()
 
817
 
 
818
    origin = origins[Options.fromdistro]
 
819
    origin["suite"] = Options.fromsuite
 
820
    origin["component"] = Options.fromcomponent
 
821
 
 
822
    Sources = read_Sources("Sources", origin)
 
823
    Suite = read_current_source(
 
824
        Options.tosuite, Options.tocomponent, arguments)
 
825
    current_binaries = read_current_binaries(Options.tosuite)
 
826
    do_diff(Sources, Suite, origin, arguments, current_binaries)
778
827
 
779
828
 
780
829
if __name__ == '__main__':
781
 
    SyncSourceScript('sync-source', 'ro').lock_and_run()
 
830
    main()