Coverage for dak/process_upload.py: 83%

297 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2025-08-26 22:11 +0000

1#! /usr/bin/env python3 

2 

3""" 

4Checks Debian packages from Incoming 

5@contact: Debian FTP Master <ftpmaster@debian.org> 

6@copyright: 2000, 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org> 

7@copyright: 2009 Joerg Jaspert <joerg@debian.org> 

8@copyright: 2009 Mark Hymers <mhy@debian.org> 

9@copyright: 2009 Frank Lichtenheld <djpig@debian.org> 

10@license: GNU General Public License version 2 or later 

11""" 

12 

13# This program is free software; you can redistribute it and/or modify 

14# it under the terms of the GNU General Public License as published by 

15# the Free Software Foundation; either version 2 of the License, or 

16# (at your option) any later version. 

17 

18# This program is distributed in the hope that it will be useful, 

19# but WITHOUT ANY WARRANTY; without even the implied warranty of 

20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21# GNU General Public License for more details. 

22 

23# You should have received a copy of the GNU General Public License 

24# along with this program; if not, write to the Free Software 

25# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

26 

27# based on process-unchecked and process-accepted 

28 

29## pu|pa: locking (daily.lock) 

30## pu|pa: parse arguments -> list of changes files 

31## pa: initialize urgency log 

32## pu|pa: sort changes list 

33 

34## foreach changes: 

35### pa: load dak file 

36## pu: copy CHG to tempdir 

37## pu: check CHG signature 

38## pu: parse changes file 

39## pu: checks: 

40## pu: check distribution (mappings, rejects) 

41## pu: copy FILES to tempdir 

42## pu: check whether CHG already exists in CopyChanges 

43## pu: check whether FILES already exist in one of the policy queues 

44## for deb in FILES: 

45## pu: extract control information 

46## pu: various checks on control information 

47## pu|pa: search for source (in CHG, projectb, policy queues) 

48## pu|pa: check whether "Version" fulfills target suite requirements/suite propagation 

49## pu|pa: check whether deb already exists in the pool 

50## for src in FILES: 

51## pu: various checks on filenames and CHG consistency 

52## pu: if isdsc: check signature 

53## for file in FILES: 

54## pu: various checks 

55## pu: NEW? 

56## //pu: check whether file already exists in the pool 

57## pu: store what "Component" the package is currently in 

58## pu: check whether we found everything we were looking for in CHG 

59## pu: check the DSC: 

60## pu: check whether we need and have ONE DSC 

61## pu: parse the DSC 

62## pu: various checks //maybe drop some of the in favor of lintian 

63## pu|pa: check whether "Version" fulfills target suite requirements/suite propagation 

64## pu: check whether DSC_FILES is consistent with "Format" 

65## for src in DSC_FILES: 

66## pu|pa: check whether file already exists in the pool (with special handling for .orig.tar.gz) 

67## pu: create new tempdir 

68## pu: create symlink mirror of source 

69## pu: unpack source 

70## pu: extract changelog information for BTS 

71## //pu: create missing .orig symlink 

72## pu: check with lintian 

73## for file in FILES: 

74## pu: check checksums and sizes 

75## for file in DSC_FILES: 

76## pu: check checksums and sizes 

77## pu: CHG: check urgency 

78## for deb in FILES: 

79## pu: extract contents list and check for dubious timestamps 

80## pu: check that the uploader is actually allowed to upload the package 

81### pa: install: 

82### if stable_install: 

83### pa: remove from p-u 

84### pa: add to stable 

85### pa: move CHG to morgue 

86### pa: append data to ChangeLog 

87### pa: send mail 

88### pa: remove .dak file 

89### else: 

90### pa: add dsc to db: 

91### for file in DSC_FILES: 

92### pa: add file to file 

93### pa: add file to dsc_files 

94### pa: create source entry 

95### pa: update source associations 

96### pa: update src_uploaders 

97### for deb in FILES: 

98### pa: add deb to db: 

99### pa: add file to file 

100### pa: find source entry 

101### pa: create binaries entry 

102### pa: update binary associations 

103### pa: .orig component move 

104### pa: move files to pool 

105### pa: save CHG 

106### pa: move CHG to done/ 

107### pa: change entry in queue_build 

108## pu: use dispatch table to choose target queue: 

109## if NEW: 

110## pu: write .dak file 

111## pu: move to NEW 

112## pu: send mail 

113## elsif AUTOBYHAND: 

114## pu: run autobyhand script 

115## pu: if stuff left, do byhand or accept 

116## elsif targetqueue in (oldstable, stable, embargo, unembargo): 

117## pu: write .dak file 

118## pu: check overrides 

119## pu: move to queue 

120## pu: send mail 

121## else: 

122## pu: write .dak file 

123## pu: move to ACCEPTED 

124## pu: send mails 

125## pu: create files for BTS 

126## pu: create entry in queue_build 

127## pu: check overrides 

128 

129# Integrity checks 

130## GPG 

131## Parsing changes (check for duplicates) 

132## Parse dsc 

133## file list checks 

134 

135# New check layout (TODO: Implement) 

136## Permission checks 

137### suite mappings 

138### ACLs 

139### version checks (suite) 

140### override checks 

141 

142## Source checks 

143### copy orig 

144### unpack 

145### BTS changelog 

146### src contents 

147### lintian 

148### urgency log 

149 

150## Binary checks 

151### timestamps 

152### control checks 

153### src relation check 

154### contents 

155 

156## Database insertion (? copy from stuff) 

157### BYHAND / NEW / Policy queues 

158### Pool 

159 

160## Queue builds 

161 

162import datetime 

163import errno 

164import fcntl 

165import functools 

166import os 

167import sys 

168import time 

169import traceback 

170from collections.abc import Iterable 

171from typing import NoReturn 

172 

173import apt_pkg 

174 

175import daklib.announce 

176import daklib.archive 

177import daklib.checks 

178import daklib.upload 

179import daklib.utils as utils 

180from daklib import daklog 

181from daklib.config import Config 

182from daklib.dbconn import DBConn, Keyring, SignatureHistory 

183from daklib.regexes import re_default_answer 

184from daklib.summarystats import SummaryStats 

185from daklib.urgencylog import UrgencyLog 

186 

187############################################################################### 

188 

189Options = None 

190Logger = None 

191 

192############################################################################### 

193 

194 

195def usage(exit_code=0) -> NoReturn: 

196 print( 

197 """Usage: dak process-upload [OPTION]... [CHANGES]... 

198 -a, --automatic automatic run 

199 -d, --directory <DIR> process uploads in <DIR> 

200 -h, --help show this help and exit. 

201 -n, --no-action don't do anything 

202 -p, --no-lock don't check lockfile !! for cron.daily only !! 

203 -s, --no-mail don't send any mail 

204 -V, --version display the version number and exit""" 

205 ) 

206 sys.exit(exit_code) 

207 

208 

209############################################################################### 

210 

211 

212def try_or_reject(function): 

213 """Try to call function or reject the upload if that fails""" 

214 

215 @functools.wraps(function) 

216 def wrapper(directory: str, upload: daklib.archive.ArchiveUpload, *args, **kwargs): 

217 reason = "No exception caught. This should not happen." 

218 

219 try: 

220 return function(directory, upload, *args, **kwargs) 

221 except (daklib.archive.ArchiveException, daklib.checks.Reject) as e: 

222 reason = str(e) 

223 except Exception: 

224 reason = "There was an uncaught exception when processing your upload:\n{0}\nAny original reject reason follows below.".format( 

225 traceback.format_exc() 

226 ) 

227 

228 try: 

229 upload.rollback() 

230 return real_reject(directory, upload, reason=reason) 

231 except Exception: 

232 reason = "In addition there was an exception when rejecting the package:\n{0}\nPrevious reasons:\n{1}".format( 

233 traceback.format_exc(), reason 

234 ) 

235 upload.rollback() 

236 return real_reject(directory, upload, reason=reason, notify=False) 

237 

238 raise Exception( 

239 "Rejecting upload failed after multiple tries. Giving up. Last reason:\n{0}".format( 

240 reason 

241 ) 

242 ) 

243 

244 return wrapper 

245 

246 

247def get_processed_upload( 

248 upload: daklib.archive.ArchiveUpload, 

249) -> daklib.announce.ProcessedUpload: 

250 changes = upload.changes 

251 control = upload.changes.changes 

252 

253 pu = daklib.announce.ProcessedUpload() 

254 

255 pu.maintainer = control.get("Maintainer") 

256 pu.changed_by = control.get("Changed-By") 

257 pu.fingerprint = changes.primary_fingerprint 

258 pu.authorized_by_fingerprint = upload.authorized_by_fingerprint.fingerprint 

259 

260 pu.suites = upload.final_suites or [] 

261 pu.from_policy_suites = [] 

262 

263 with open(upload.changes.path, "r") as fd: 

264 pu.changes = fd.read() 

265 pu.changes_filename = upload.changes.filename 

266 pu.sourceful = upload.changes.sourceful 

267 pu.source = control.get("Source") 

268 pu.version = control.get("Version") 

269 pu.architecture = control.get("Architecture") 

270 pu.bugs = changes.closed_bugs 

271 

272 pu.program = "process-upload" 

273 

274 pu.warnings = upload.warnings 

275 

276 return pu 

277 

278 

279@try_or_reject 

280def accept(directory: str, upload: daklib.archive.ArchiveUpload) -> None: 

281 cnf = Config() 

282 

283 Logger.log(["ACCEPT", upload.changes.filename]) 

284 print("ACCEPT") 

285 

286 upload.install() 

287 utils.process_buildinfos( 

288 upload.directory, upload.changes.buildinfo_files, upload.transaction.fs, Logger 

289 ) 

290 

291 accepted_to_real_suite = any( 

292 suite.policy_queue is None for suite in upload.final_suites 

293 ) 

294 sourceful_upload = upload.changes.sourceful 

295 

296 control = upload.changes.changes 

297 if sourceful_upload and not Options["No-Action"]: 

298 urgency = control.get("Urgency") 

299 # As per policy 5.6.17, the urgency can be followed by a space and a 

300 # comment. Extract only the urgency from the string. 

301 if " " in urgency: 301 ↛ 302line 301 didn't jump to line 302, because the condition on line 301 was never true

302 urgency, comment = urgency.split(" ", 1) 

303 if urgency not in cnf.value_list("Urgency::Valid"): 303 ↛ 304line 303 didn't jump to line 304, because the condition on line 303 was never true

304 urgency = cnf["Urgency::Default"] 

305 UrgencyLog().log(control["Source"], control["Version"], urgency) 

306 

307 pu = get_processed_upload(upload) 

308 daklib.announce.announce_accept(pu) 

309 

310 # Move .changes to done, but only for uploads that were accepted to a 

311 # real suite. process-policy will handle this for uploads to queues. 

312 if accepted_to_real_suite: 

313 src = os.path.join(upload.directory, upload.changes.filename) 

314 

315 now = datetime.datetime.now() 

316 donedir = os.path.join(cnf["Dir::Done"], now.strftime("%Y/%m/%d")) 

317 dst = os.path.join(donedir, upload.changes.filename) 

318 dst = utils.find_next_free(dst) 

319 

320 upload.transaction.fs.copy(src, dst, mode=0o644) 

321 

322 SummaryStats().accept_count += 1 

323 SummaryStats().accept_bytes += upload.changes.bytes 

324 

325 

326@try_or_reject 

327def accept_to_new(directory: str, upload: daklib.archive.ArchiveUpload) -> None: 

328 

329 Logger.log(["ACCEPT-TO-NEW", upload.changes.filename]) 

330 print("ACCEPT-TO-NEW") 

331 

332 upload.install_to_new() 

333 # TODO: tag bugs pending 

334 

335 pu = get_processed_upload(upload) 

336 daklib.announce.announce_new(pu) 

337 

338 SummaryStats().accept_count += 1 

339 SummaryStats().accept_bytes += upload.changes.bytes 

340 

341 

342@try_or_reject 

343def reject( 

344 directory: str, upload: daklib.archive.ArchiveUpload, reason=None, notify=True 

345) -> None: 

346 real_reject(directory, upload, reason, notify) 

347 

348 

349def real_reject( 

350 directory: str, upload: daklib.archive.ArchiveUpload, reason=None, notify=True 

351) -> None: 

352 # XXX: rejection itself should go to daklib.archive.ArchiveUpload 

353 cnf = Config() 

354 

355 Logger.log(["REJECT", upload.changes.filename]) 

356 print("REJECT") 

357 

358 fs = upload.transaction.fs 

359 rejectdir = cnf["Dir::Reject"] 

360 

361 files = [f.filename for f in upload.changes.files.values()] 

362 files.append(upload.changes.filename) 

363 

364 for fn in files: 

365 src = os.path.join(upload.directory, fn) 

366 dst = utils.find_next_free(os.path.join(rejectdir, fn)) 

367 if not os.path.exists(src): 

368 continue 

369 fs.copy(src, dst) 

370 

371 if upload.reject_reasons is not None: 371 ↛ 376line 371 didn't jump to line 376, because the condition on line 371 was never false

372 if reason is None: 372 ↛ 374line 372 didn't jump to line 374, because the condition on line 372 was never false

373 reason = "" 

374 reason = reason + "\n" + "\n".join(upload.reject_reasons) 

375 

376 if reason is None: 376 ↛ 377line 376 didn't jump to line 377, because the condition on line 376 was never true

377 reason = "(Unknown reason. Please check logs.)" 

378 

379 dst = utils.find_next_free( 

380 os.path.join(rejectdir, "{0}.reason".format(upload.changes.filename)) 

381 ) 

382 fh = fs.create(dst) 

383 fh.write(reason) 

384 fh.close() 

385 

386 if notify: 386 ↛ 390line 386 didn't jump to line 390, because the condition on line 386 was never false

387 pu = get_processed_upload(upload) 

388 daklib.announce.announce_reject(pu, reason) 

389 

390 SummaryStats().reject_count += 1 

391 

392 

393############################################################################### 

394 

395 

396def action(directory: str, upload: daklib.archive.ArchiveUpload) -> bool: 

397 changes = upload.changes 

398 processed = True 

399 

400 global Logger 

401 

402 cnf = Config() 

403 

404 okay = upload.check() 

405 

406 try: 

407 summary = changes.changes.get("Changes", "") 

408 except UnicodeDecodeError as e: 

409 summary = "Reading changes failed: %s" % (e) 

410 # the upload checks should have detected this, but make sure this 

411 # upload gets rejected in any case 

412 upload.reject_reasons.append(summary) 

413 

414 package_info = [] 

415 if okay: 

416 if changes.source is not None: 

417 package_info.append("source:{0}".format(changes.source.dsc["Source"])) 

418 for binary in changes.binaries: 

419 package_info.append("binary:{0}".format(binary.control["Package"])) 

420 

421 (prompt, answer) = ("", "XXX") 

422 if Options["No-Action"] or Options["Automatic"]: 422 ↛ 425line 422 didn't jump to line 425, because the condition on line 422 was never false

423 answer = "S" 

424 

425 print(summary) 

426 print() 

427 print("\n".join(package_info)) 

428 print() 

429 if len(upload.warnings) > 0: 

430 print("\n".join(upload.warnings)) 

431 print() 

432 

433 if len(upload.reject_reasons) > 0: 

434 print("Reason:") 

435 print("\n".join(upload.reject_reasons)) 

436 print() 

437 

438 path = os.path.join(directory, changes.filename) 

439 created = os.stat(path).st_mtime 

440 now = time.time() 

441 too_new = now - created < int(cnf["Dinstall::SkipTime"]) 

442 

443 if too_new: 

444 print("SKIP (too new)") 

445 prompt = "[S]kip, Quit ?" 

446 else: 

447 prompt = "[R]eject, Skip, Quit ?" 

448 if Options["Automatic"]: 448 ↛ 459line 448 didn't jump to line 459, because the condition on line 448 was never false

449 answer = "R" 

450 elif upload.new: 

451 prompt = "[N]ew, Skip, Quit ?" 

452 if Options["Automatic"]: 452 ↛ 459line 452 didn't jump to line 459, because the condition on line 452 was never false

453 answer = "N" 

454 else: 

455 prompt = "[A]ccept, Skip, Quit ?" 

456 if Options["Automatic"]: 456 ↛ 459line 456 didn't jump to line 459, because the condition on line 456 was never false

457 answer = "A" 

458 

459 while prompt.find(answer) == -1: 459 ↛ 460line 459 didn't jump to line 460, because the condition on line 459 was never true

460 answer = utils.input_or_exit(prompt) 

461 m = re_default_answer.match(prompt) 

462 if answer == "": 

463 answer = m.group(1) 

464 answer = answer[:1].upper() 

465 

466 if answer == "R": 

467 reject(directory, upload) 

468 elif answer == "A": 

469 # upload.try_autobyhand must not be run with No-Action. 

470 if Options["No-Action"]: 470 ↛ 471line 470 didn't jump to line 471, because the condition on line 470 was never true

471 accept(directory, upload) 

472 elif upload.try_autobyhand(): 472 ↛ 475line 472 didn't jump to line 475, because the condition on line 472 was never false

473 accept(directory, upload) 

474 else: 

475 print("W: redirecting to BYHAND as automatic processing failed.") 

476 accept_to_new(directory, upload) 

477 elif answer == "N": 

478 accept_to_new(directory, upload) 

479 elif answer == "Q": 479 ↛ 480line 479 didn't jump to line 480, because the condition on line 479 was never true

480 sys.exit(0) 

481 elif answer == "S": 481 ↛ 484line 481 didn't jump to line 484, because the condition on line 481 was never false

482 processed = False 

483 

484 if not Options["No-Action"]: 484 ↛ 487line 484 didn't jump to line 487, because the condition on line 484 was never false

485 upload.commit() 

486 

487 return processed 

488 

489 

490############################################################################### 

491 

492 

493def unlink_if_exists(path: str) -> None: 

494 try: 

495 os.unlink(path) 

496 except OSError as e: 

497 if e.errno != errno.ENOENT: 497 ↛ 498line 497 didn't jump to line 498, because the condition on line 497 was never true

498 raise 

499 

500 

501def process_it( 

502 directory: str, changes: daklib.upload.Changes, keyrings: list[str] 

503) -> None: 

504 global Logger 

505 

506 print("\n{0}\n".format(changes.filename)) 

507 Logger.log(["Processing changes file", changes.filename]) 

508 

509 with daklib.archive.ArchiveUpload(directory, changes, keyrings) as upload: 

510 processed = action(directory, upload) 

511 if processed and not Options["No-Action"]: 

512 session = DBConn().session() 

513 history = SignatureHistory.from_signed_file(upload.changes) 

514 if history.query(session) is None: 514 ↛ 517line 514 didn't jump to line 517, because the condition on line 514 was never false

515 session.add(history) 

516 session.commit() 

517 session.close() 

518 

519 unlink_if_exists(os.path.join(directory, changes.filename)) 

520 for fn in changes.files: 

521 unlink_if_exists(os.path.join(directory, fn)) 

522 

523 

524############################################################################### 

525 

526 

527def process_changes(changes_filenames: Iterable[str]): 

528 session = DBConn().session() 

529 keyrings = session.query(Keyring).filter_by(active=True).order_by(Keyring.priority) 

530 keyring_files = [k.keyring_name for k in keyrings] 

531 session.close() 

532 

533 changes = [] 

534 for fn in changes_filenames: 

535 try: 

536 directory, filename = os.path.split(fn) 

537 c = daklib.upload.Changes(directory, filename, keyring_files) 

538 changes.append([directory, c]) 

539 except Exception as e: 

540 try: 

541 Logger.log( 

542 [ 

543 filename, 

544 "Error while loading changes file {0}: {1}".format(fn, e), 

545 ] 

546 ) 

547 except Exception as e: 

548 Logger.log( 

549 [ 

550 filename, 

551 "Error while loading changes file {0}, with additional error while printing exception: {1}".format( 

552 fn, repr(e) 

553 ), 

554 ] 

555 ) 

556 

557 changes.sort(key=lambda x: x[1]) 

558 

559 for directory, c in changes: 

560 process_it(directory, c, keyring_files) 

561 

562 

563############################################################################### 

564 

565 

566def main(): 

567 global Options, Logger 

568 

569 cnf = Config() 

570 summarystats = SummaryStats() 

571 

572 Arguments = [ 

573 ("a", "automatic", "Dinstall::Options::Automatic"), 

574 ("h", "help", "Dinstall::Options::Help"), 

575 ("n", "no-action", "Dinstall::Options::No-Action"), 

576 ("p", "no-lock", "Dinstall::Options::No-Lock"), 

577 ("s", "no-mail", "Dinstall::Options::No-Mail"), 

578 ("d", "directory", "Dinstall::Options::Directory", "HasArg"), 

579 ] 

580 

581 for i in [ 

582 "automatic", 

583 "help", 

584 "no-action", 

585 "no-lock", 

586 "no-mail", 

587 "version", 

588 "directory", 

589 ]: 

590 key = "Dinstall::Options::%s" % i 

591 if key not in cnf: 

592 cnf[key] = "" 

593 

594 changes_files = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) 

595 Options = cnf.subtree("Dinstall::Options") 

596 

597 if Options["Help"]: 

598 usage() 

599 

600 # -n/--dry-run invalidates some other options which would involve things happening 

601 if Options["No-Action"]: 601 ↛ 602line 601 didn't jump to line 602, because the condition on line 601 was never true

602 Options["Automatic"] = "" 

603 

604 # Obtain lock if not in no-action mode and initialize the log 

605 if not Options["No-Action"]: 605 ↛ 624line 605 didn't jump to line 624, because the condition on line 605 was never false

606 lock_fd = os.open( 

607 os.path.join(cnf["Dir::Lock"], "process-upload.lock"), 

608 os.O_RDWR | os.O_CREAT, 

609 ) 

610 try: 

611 fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 

612 except OSError as e: 

613 if e.errno in (errno.EACCES, errno.EAGAIN): 

614 utils.fubar( 

615 "Couldn't obtain lock; assuming another 'dak process-upload' is already running." 

616 ) 

617 else: 

618 raise 

619 

620 # Initialise UrgencyLog() - it will deal with the case where we don't 

621 # want to log urgencies 

622 urgencylog = UrgencyLog() 

623 

624 Logger = daklog.Logger("process-upload", Options["No-Action"]) 

625 

626 # If we have a directory flag, use it to find our files 

627 if cnf["Dinstall::Options::Directory"] != "": 627 ↛ 647line 627 didn't jump to line 647, because the condition on line 627 was never false

628 # Note that we clobber the list of files we were given in this case 

629 # so warn if the user has done both 

630 if len(changes_files) > 0: 630 ↛ 631line 630 didn't jump to line 631, because the condition on line 630 was never true

631 utils.warn("Directory provided so ignoring files given on command line") 

632 

633 changes_files = utils.get_changes_files(cnf["Dinstall::Options::Directory"]) 

634 # FIXME: quick hack to NOT have p-u run for ages, if some binnmu fun uploads thousands of changes 

635 # might want to make the number configurable at some point 

636 if len(changes_files) > 200: 636 ↛ 637line 636 didn't jump to line 637, because the condition on line 636 was never true

637 import random 

638 

639 changes_files = random.sample(changes_files, k=200) 

640 Logger.log( 

641 [ 

642 "Using changes files from directory", 

643 cnf["Dinstall::Options::Directory"], 

644 len(changes_files), 

645 ] 

646 ) 

647 elif not len(changes_files) > 0: 

648 utils.fubar("No changes files given and no directory specified") 

649 else: 

650 Logger.log(["Using changes files from command-line", len(changes_files)]) 

651 

652 process_changes(changes_files) 

653 

654 if summarystats.accept_count: 

655 sets = "set" 

656 if summarystats.accept_count > 1: 

657 sets = "sets" 

658 print( 

659 "Installed %d package %s, %s." 

660 % ( 

661 summarystats.accept_count, 

662 sets, 

663 utils.size_type(int(summarystats.accept_bytes)), 

664 ) 

665 ) 

666 Logger.log(["total", summarystats.accept_count, summarystats.accept_bytes]) 

667 

668 if summarystats.reject_count: 

669 sets = "set" 

670 if summarystats.reject_count > 1: 

671 sets = "sets" 

672 print("Rejected %d package %s." % (summarystats.reject_count, sets)) 

673 Logger.log(["rejected", summarystats.reject_count]) 

674 

675 if not Options["No-Action"]: 675 ↛ 678line 675 didn't jump to line 678, because the condition on line 675 was never false

676 urgencylog.close() 

677 

678 Logger.close() 

679 

680 

681############################################################################### 

682 

683 

684if __name__ == "__main__": 

685 main()