1#!@TOOLS_PYTHON@ 2# 3# CDDL HEADER START 4# 5# The contents of this file are subject to the terms of the 6# Common Development and Distribution License (the "License"). 7# You may not use this file except in compliance with the License. 8# 9# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 10# or http://www.opensolaris.org/os/licensing. 11# See the License for the specific language governing permissions 12# and limitations under the License. 13# 14# When distributing Covered Code, include this CDDL HEADER in each 15# file and include the License file at usr/src/OPENSOLARIS.LICENSE. 16# If applicable, add the following below this CDDL HEADER, with the 17# fields enclosed by brackets "[]" replaced with your own identifying 18# information: Portions Copyright [yyyy] [name of copyright owner] 19# 20# CDDL HEADER END 21# 22# Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. 23# Copyright 2020 OmniOS Community Edition (OmniOSce) Association. 24# 25 26# 27# wsdiff(1) is a tool that can be used to determine which compiled objects 28# have changed as a result of a given source change. Developers backporting 29# new features, RFEs and bug fixes need to be able to identify the set of 30# patch deliverables necessary for feature/fix realization on a patched system. 31# 32# The tool works by comparing objects in two trees/proto areas (one build with, 33# and without the source changes. 34# 35# Using wsdiff(1) is fairly simple: 36# - Bringover to a fresh workspace 37# - Perform a full non-debug build (clobber if workspace isn't fresh) 38# - Move the proto area aside, call it proto.old, or something. 39# - Integrate your changes to the workspace 40# - Perform another full non-debug clobber build. 41# - Use wsdiff(1) to see what changed: 42# $ wsdiff proto.old proto 43# 44# By default, wsdiff will print the list of changed objects / deliverables to 45# stdout. If a results file is specified via -r, the list of differing objects, 46# and details about why wsdiff(1) thinks they are different will be logged to 47# the results file. 48# 49# By invoking nightly(1) with the -w option to NIGHTLY_FLAGS, nightly(1) will 50# use wsdiff(1) to report on what objects changed since the last build. 51# 52# For patch deliverable purposes, it's advised to have nightly do a clobber, 53# non-debug build. 54# 55# Think about the results. Was something flagged that you don't expect? Go look 56# at the results file to see details about the differences. 57# 58# Use the -i option in conjunction with -v and -V to dive deeper and have 59# wsdiff(1) report with more verbosity. 60# 61# Usage: wsdiff [-dstuvV] [-U lines] [-r results ] [-i filelist ] old new 62# 63# Where "old" is the path to the proto area build without the changes, and 64# "new" is the path to the proto area built with the changes. The following 65# options are supported: 66# 67# -d Print debug messages about the progress 68# -i Tell wsdiff which objects to compare via an input file list 69# -r Log results and observed differences 70# -s Produce sorted list of differences 71# -t Use onbld tools in $SRC/tools 72# -u Produce unified diff output 73# -U Produce unified diff output with <lines> lines of context 74# -v Do not truncate observed diffs in results 75# -V Log *all* ELF sect diffs vs. logging the first diff found 76 77from __future__ import print_function 78import datetime, fnmatch, getopt, os, profile, io, subprocess 79import re, resource, select, shutil, signal, string, struct, sys, tempfile 80import time, threading 81from stat import * 82 83PY3 = sys.version_info[0] == 3 84 85# Human readable diffs truncated by default if longer than this 86# Specifying -v on the command line will override 87diffs_sz_thresh = 4096 88diff_args = '' 89 90# Lock name Provides exclusive access to 91# --------------+------------------------------------------------ 92# output_lock standard output or temporary file (difference()) 93# log_lock the results file (log_difference()) 94# wset_lock changedFiles list (workerThread()) 95output_lock = threading.Lock() 96log_lock = threading.Lock() 97wset_lock = threading.Lock() 98 99# Variable for thread control 100keep_processing = True 101 102# Default search path for wsdiff 103wsdiff_path = [ "/usr/bin", 104 "/usr/ccs/bin", 105 "/lib/svc/bin", 106 "/opt/onbld/bin" ] 107 108# These are objects that wsdiff will notice look different, but will not report. 109# Existence of an exceptions list, and adding things here is *dangerous*, 110# and therefore the *only* reasons why anything would be listed here is because 111# the objects do not build deterministically, yet we *cannot* fix this. 112# 113wsdiff_exceptions = [ 114] 115 116# Path to genunix, used for CTF diff 117genunix = "/kernel/amd64/genunix" 118 119if PY3: 120 def getoutput(cmd): 121 import shlex, tempfile 122 f, fpath = tempfile.mkstemp() 123 status = os.system("{ " + cmd + "; } >" + 124 shlex.quote(fpath) + " 2>&1") 125 returncode = os.WEXITSTATUS(status) 126 with os.fdopen(f, mode="r", errors="ignore") as tfile: 127 output = tfile.read() 128 os.unlink(fpath) 129 if output[-1:] == '\n': 130 output = output[:-1] 131 return returncode, output 132else: 133 import commands 134 getoutput = commands.getstatusoutput 135 136##### 137# Logging routines 138# 139 140# Debug message to be printed to the screen, and the log file 141def debug(msg) : 142 143 # Add prefix to highlight debugging message 144 msg = "## " + msg 145 if debugon : 146 output_lock.acquire() 147 print(msg) 148 sys.stdout.flush() 149 output_lock.release() 150 if logging : 151 log_lock.acquire() 152 print(msg, file=log) 153 log.flush() 154 log_lock.release() 155 156# Informational message to be printed to the screen, and the log file 157def info(msg) : 158 159 output_lock.acquire() 160 print(msg) 161 sys.stdout.flush() 162 output_lock.release() 163 if logging : 164 log_lock.acquire() 165 print(msg, file=log) 166 log.flush() 167 log_lock.release() 168 169# Error message to be printed to the screen, and the log file 170def error(msg) : 171 172 output_lock.acquire() 173 print("ERROR: " + msg, file=sys.stderr) 174 sys.stderr.flush() 175 output_lock.release() 176 if logging : 177 log_lock.acquire() 178 print("ERROR: " + msg, file=log) 179 log.flush() 180 log_lock.release() 181 182# Informational message to be printed only to the log, if there is one. 183def v_info(msg) : 184 185 if logging : 186 log_lock.acquire() 187 print(msg, file=log) 188 log.flush() 189 log_lock.release() 190 191# 192# Flag a detected file difference 193# Display the fileName to stdout, and log the difference 194# 195def difference(f, dtype, diffs) : 196 197 if f in wsdiff_exceptions : 198 return 199 200 output_lock.acquire() 201 if o_sorted : 202 differentFiles.append(f) 203 else: 204 print(f) 205 sys.stdout.flush() 206 output_lock.release() 207 208 log_difference(f, dtype, diffs) 209 210# 211# Do the actual logging of the difference to the results file 212# 213def log_difference(f, dtype, diffs) : 214 215 if logging : 216 log_lock.acquire() 217 print(f, file=log) 218 print("NOTE: " + dtype + " difference detected.", file=log) 219 220 difflen = len(diffs) 221 if difflen > 0 : 222 print('', file=log) 223 224 if not vdiffs and difflen > diffs_sz_thresh : 225 print(diffs[:diffs_sz_thresh], file=log) 226 print("... truncated due to length: " + 227 "use -v to override ...", file=log) 228 else : 229 print(diffs, file=log) 230 print('\n', file=log) 231 log.flush() 232 log_lock.release() 233 234 235##### 236# diff generating routines 237# 238 239# 240# Return human readable diffs from two files 241# 242def diffFileData(tmpf1, tmpf2) : 243 244 binaries = False 245 246 # Filter the data through od(1) if the data is detected 247 # as being binary 248 if isBinary(tmpf1) or isBinary(tmpf2) : 249 binaries = True 250 tmp_od1 = tmpf1 + ".od" 251 tmp_od2 = tmpf2 + ".od" 252 253 cmd = od_cmd + " -c -t x4" + " " + tmpf1 + " > " + tmp_od1 254 os.system(cmd) 255 cmd = od_cmd + " -c -t x4" + " " + tmpf2 + " > " + tmp_od2 256 os.system(cmd) 257 258 tmpf1 = tmp_od1 259 tmpf2 = tmp_od2 260 261 dcmd = "{} {} {} {}".format(diff_cmd, diff_args, tmpf1, tmpf2) 262 try: 263 rc, data = getoutput(dcmd) 264 if rc == 0: 265 # No differences found 266 data = '' 267 # If producing unified output, strip the first two lines 268 # which just show the temporary file names. 269 if diff_args: 270 data = data.split("\n", 2)[-1] 271 272 # Remove the temp files as we no longer need them. 273 if binaries : 274 try: 275 os.unlink(tmp_od1) 276 except OSError as e: 277 error("diffFileData: unlink failed %s" % e) 278 try: 279 os.unlink(tmp_od2) 280 except OSError as e: 281 error("diffFileData: unlink failed %s" % e) 282 except: 283 error("failed to get output of command: " + dcmd) 284 285 # Send exception for the failed command up 286 raise 287 return 288 289 return data 290 291##### 292# Misc utility functions 293# 294 295# Prune off the leading prefix from string s 296def str_prefix_trunc(s, prefix) : 297 snipLen = len(prefix) 298 return s[snipLen:] 299 300# 301# Prune off leading proto path goo (if there is one) to yield 302# the deliverable's eventual path relative to root 303# e.g. proto.base/root_sparc/usr/src/cmd/prstat => usr/src/cmd/prstat 304# 305def fnFormat(fn) : 306 root_arch_str = "root_" + arch 307 308 pos = fn.find(root_arch_str) 309 if pos == -1 : 310 return fn 311 312 pos = fn.find("/", pos) 313 if pos == -1 : 314 return fn 315 316 return fn[pos + 1:] 317 318# 319# Find the path to a proto root given the name of a file or directory under it 320# e.g. proto.base/root_i386-nd/usr/bin => proto.base/root_i386-nd 321# 322def protoroot(fn): 323 root_arch_str = "root_" + arch 324 325 pos = fn.find(root_arch_str) 326 if pos == -1: 327 return None 328 329 pos = fn.find("/", pos) 330 if pos == -1: 331 return fn 332 333 return fn[:pos] 334 335 336##### 337# Usage / argument processing 338# 339 340# 341# Display usage message 342# 343def usage() : 344 sys.stdout.flush() 345 print("""Usage: wsdiff [-dstuvV] [-U lines] [-r results ] [-i filelist ] old new 346 -d Print debug messages about the progress 347 -i Tell wsdiff which objects to compare via an input file list 348 -r Log results and observed differences 349 -s Produce sorted list of differences 350 -t Use onbld tools in $SRC/tools 351 -u Produce unified diff output 352 -U Produce unified diff output with <lines> lines of context 353 -v Do not truncate observed diffs in results 354 -V Log *all* ELF sect diffs vs. logging the first diff found""", 355 file=sys.stderr) 356 sys.exit(1) 357 358# 359# Process command line options 360# 361def args() : 362 363 global debugon 364 global logging 365 global vdiffs 366 global reportAllSects 367 global o_sorted 368 global diff_args 369 370 validOpts = 'di:r:uU:vVst?' 371 372 baseRoot = "" 373 ptchRoot = "" 374 fileNamesFile = "" 375 results = "" 376 localTools = False 377 378 # getopt.getopt() returns: 379 # an option/value tuple 380 # a list of remaining non-option arguments 381 # 382 # A correct wsdiff invocation will have exactly two non option 383 # arguments, the paths to the base (old), ptch (new) proto areas 384 try: 385 optlist, args = getopt.getopt(sys.argv[1:], validOpts) 386 except getopt.error as val: 387 usage() 388 389 if len(args) != 2 : 390 usage(); 391 392 for opt,val in optlist : 393 if opt == '-d' : 394 debugon = True 395 elif opt == '-i' : 396 fileNamesFile = val 397 elif opt == '-r' : 398 results = val 399 logging = True 400 elif opt == '-s' : 401 o_sorted = True 402 elif opt == '-u' : 403 diff_args = '-u' 404 elif opt == '-U' : 405 diff_args = '-U' + str(val) 406 elif opt == '-v' : 407 vdiffs = True 408 elif opt == '-V' : 409 reportAllSects = True 410 elif opt == '-t': 411 localTools = True 412 else: 413 usage() 414 415 baseRoot = args[0] 416 ptchRoot = args[1] 417 418 if len(baseRoot) == 0 or len(ptchRoot) == 0 : 419 usage() 420 421 if logging and len(results) == 0 : 422 usage() 423 424 if vdiffs and not logging : 425 error("The -v option requires a results file (-r)") 426 sys.exit(1) 427 428 if reportAllSects and not logging : 429 error("The -V option requires a results file (-r)") 430 sys.exit(1) 431 432 # alphabetical order 433 return baseRoot, fileNamesFile, localTools, ptchRoot, results 434 435##### 436# File identification 437# 438 439# 440# Identify the file type. 441# If it's not ELF, use the file extension to identify 442# certain file types that require special handling to 443# compare. Otherwise just return a basic "ASCII" type. 444# 445def getTheFileType(f) : 446 447 extensions = { 'a' : 'ELF Object Archive', 448 'jar' : 'Java Archive', 449 'html' : 'HTML', 450 'ln' : 'Lint Library', 451 'db' : 'Sqlite Database' } 452 453 try: 454 if os.stat(f)[ST_SIZE] == 0 : 455 return 'ASCII' 456 except: 457 error("failed to stat " + f) 458 return 'Error' 459 460 if isELF(f) == 1 : 461 return 'ELF' 462 463 fnamelist = f.split('.') 464 if len(fnamelist) > 1 : # Test the file extension 465 extension = fnamelist[-1] 466 if extension in extensions.keys(): 467 return extensions[extension] 468 469 return 'ASCII' 470 471# 472# Return non-zero if "f" is an ELF file 473# 474elfmagic = b'\177ELF' 475def isELF(f) : 476 try: 477 with open(f, mode='rb') as fd: 478 magic = fd.read(len(elfmagic)) 479 480 if magic == elfmagic : 481 return 1 482 except: 483 pass 484 return 0 485 486# 487# Return non-zero is "f" is binary. 488# Consider the file to be binary if it contains any null characters 489# 490def isBinary(f) : 491 try: 492 with open(f, mode='rb') as fd: 493 s = fd.read() 494 495 if s.find(b'\0') == -1 : 496 return 0 497 except: 498 pass 499 return 1 500 501##### 502# Directory traversal and file finding 503# 504 505# 506# Return a sorted list of files found under the specified directory 507# 508def findFiles(d) : 509 for path, subdirs, files in os.walk(d) : 510 files.sort() 511 for name in files : 512 yield os.path.join(path, name) 513 514# 515# Examine all files in base, ptch 516# 517# Return a list of files appearing in both proto areas, 518# a list of new files (files found only in ptch) and 519# a list of deleted files (files found only in base) 520# 521def protoCatalog(base, ptch) : 522 523 compFiles = [] # List of files in both proto areas 524 ptchList = [] # List of file in patch proto area 525 526 newFiles = [] # New files detected 527 deletedFiles = [] # Deleted files 528 529 debug("Getting the list of files in the base area"); 530 baseFilesList = list(findFiles(base)) 531 baseStringLength = len(base) 532 debug("Found " + str(len(baseFilesList)) + " files") 533 534 debug("Getting the list of files in the patch area"); 535 ptchFilesList = list(findFiles(ptch)) 536 ptchStringLength = len(ptch) 537 debug("Found " + str(len(ptchFilesList)) + " files") 538 539 # Inventory files in the base proto area 540 debug("Determining the list of regular files in the base area"); 541 for fn in baseFilesList : 542 if os.path.islink(fn) : 543 continue 544 545 fileName = fn[baseStringLength:] 546 compFiles.append(fileName) 547 debug("Found " + str(len(compFiles)) + " files") 548 549 # Inventory files in the patch proto area 550 debug("Determining the list of regular files in the patch area"); 551 for fn in ptchFilesList : 552 if os.path.islink(fn) : 553 continue 554 555 fileName = fn[ptchStringLength:] 556 ptchList.append(fileName) 557 debug("Found " + str(len(ptchList)) + " files") 558 559 # Deleted files appear in the base area, but not the patch area 560 debug("Searching for deleted files by comparing the lists") 561 for fileName in compFiles : 562 if not fileName in ptchList : 563 deletedFiles.append(fileName) 564 debug("Found " + str(len(deletedFiles)) + " deleted files") 565 566 # Eliminate "deleted" files from the list of objects appearing 567 # in both the base and patch proto areas 568 debug("Eliminating deleted files from the list of objects") 569 for fileName in deletedFiles : 570 try: 571 compFiles.remove(fileName) 572 except: 573 error("filelist.remove() failed") 574 debug("List for comparison reduced to " + str(len(compFiles)) 575 + " files") 576 577 # New files appear in the patch area, but not the base 578 debug("Getting the list of newly added files") 579 for fileName in ptchList : 580 if not fileName in compFiles : 581 newFiles.append(fileName) 582 debug("Found " + str(len(newFiles)) + " new files") 583 584 return compFiles, newFiles, deletedFiles 585 586# 587# Examine the files listed in the input file list 588# 589# Return a list of files appearing in both proto areas, 590# a list of new files (files found only in ptch) and 591# a list of deleted files (files found only in base) 592# 593def flistCatalog(base, ptch, flist) : 594 compFiles = [] # List of files in both proto areas 595 newFiles = [] # New files detected 596 deletedFiles = [] # Deleted files 597 598 try: 599 fd = open(flist, "r") 600 except: 601 error("could not open: " + flist) 602 cleanup(1) 603 604 files = [] 605 files = fd.readlines() 606 fd.close() 607 608 for f in files : 609 ptch_present = True 610 base_present = True 611 612 if f == '\n' : 613 continue 614 615 # the fileNames have a trailing '\n' 616 f = f.rstrip() 617 618 # The objects in the file list have paths relative 619 # to $ROOT or to the base/ptch directory specified on 620 # the command line. 621 # If it's relative to $ROOT, we'll need to add back the 622 # root_`uname -p` goo we stripped off in fnFormat() 623 if os.path.exists(base + f) : 624 fn = f; 625 elif os.path.exists(base + "root_" + arch + "/" + f) : 626 fn = "root_" + arch + "/" + f 627 else : 628 base_present = False 629 630 if base_present : 631 if not os.path.exists(ptch + fn) : 632 ptch_present = False 633 else : 634 if os.path.exists(ptch + f) : 635 fn = f 636 elif os.path.exists(ptch + "root_" + arch + "/" + f) : 637 fn = "root_" + arch + "/" + f 638 else : 639 ptch_present = False 640 641 if os.path.islink(base + fn) : # ignore links 642 base_present = False 643 if os.path.islink(ptch + fn) : 644 ptch_present = False 645 646 if base_present and ptch_present : 647 compFiles.append(fn) 648 elif base_present : 649 deletedFiles.append(fn) 650 elif ptch_present : 651 newFiles.append(fn) 652 else : 653 if (os.path.islink(base + fn) and 654 os.path.islink(ptch + fn)) : 655 continue 656 error(f + " in file list, but not in either tree. " + 657 "Skipping...") 658 659 return compFiles, newFiles, deletedFiles 660 661 662# 663# Build a fully qualified path to an external tool/utility. 664# Consider the default system locations. For onbld tools, if 665# the -t option was specified, we'll try to use built tools in $SRC tools, 666# and otherwise, we'll fall back on /opt/onbld/ 667# 668def find_tool(tool) : 669 670 # First, check what was passed 671 if os.path.exists(tool) : 672 return tool 673 674 # Next try in wsdiff path 675 for pdir in wsdiff_path : 676 location = pdir + "/" + tool 677 if os.path.exists(location) : 678 return location + " " 679 680 location = pdir + "/" + arch + "/" + tool 681 if os.path.exists(location) : 682 return location + " " 683 684 error("Could not find path to: " + tool); 685 sys.exit(1); 686 687 688##### 689# ELF file comparison helper routines 690# 691 692# 693# Return a dictionary of ELF section types keyed by section name 694# 695def get_elfheader(f) : 696 697 header = {} 698 699 rc, hstring = getoutput(elfdump_cmd + " -c " + f) 700 701 if len(hstring) == 0 : 702 error("Failed to dump ELF header for " + f) 703 raise 704 return 705 706 # elfdump(1) dumps the section headers with the section name 707 # following "sh_name:", and the section type following "sh_type:" 708 sections = hstring.split("Section Header") 709 for sect in sections : 710 datap = sect.find("sh_name:"); 711 if datap == -1 : 712 continue 713 section = sect[datap:].split()[1] 714 datap = sect.find("sh_type:"); 715 if datap == -1 : 716 error("Could not get type for sect: " + section + 717 " in " + f) 718 sh_type = sect[datap:].split()[2] 719 header[section] = sh_type 720 721 return header 722 723# 724# Extract data in the specified ELF section from the given file 725# 726def extract_elf_section(f, section) : 727 728 rc, data = getoutput(dump_cmd + " -sn " + section + " " + f) 729 730 if len(data) == 0 : 731 error(dump_cmd + "yielded no data on section " + section + 732 " of " + f) 733 raise 734 return 735 736 # dump(1) displays the file name to start... 737 # get past it to the data itself 738 dbegin = data.find(":") + 1 739 data = data[dbegin:]; 740 741 return (data) 742 743# 744# Return a (hopefully meaningful) human readable set of diffs 745# for the specified ELF section between f1 and f2 746# 747# Depending on the section, various means for dumping and diffing 748# the data may be employed. 749# 750 751text_sections = [ '.text', '.init', '.fini' ] 752 753# Helper to generate the requireed commands for diffing two .SUNW_ctf 754# sections. 755def diff_ctf(f1, f2): 756 757 # Find genunix so that it can be used for parent CTF data when 758 # appropriate. 759 if diff_ctf.genunix1 is None: 760 global genunix, baseRoot, ptchRoot 761 762 d1 = protoroot(baseRoot) 763 d2 = protoroot(ptchRoot) 764 if (d1 and d2 and os.path.isfile(d1 + genunix) and 765 os.path.isfile(d2 + genunix)): 766 diff_ctf.genunix1 = d1 + genunix 767 diff_ctf.genunix2 = d2 + genunix 768 debug("CTF: Found {}".format(diff_ctf.genunix1)) 769 debug("CTF: Found {}".format(diff_ctf.genunix2)) 770 else: 771 # Could not find genunix, do the best we can. 772 error("diff_ctf: Could not find genunix. " + 773 "CTF diffs will be less useful.") 774 diff_ctf.genunix1 = diff_ctf.genunix2 = False 775 776 # Determine if this is a merged file from genunix by looking 777 # at the parent 778 rc, data = getoutput("{} -h {}".format(ctfdump_cmd, f1)) 779 if rc != 0: 780 error("Could not read CTF header: {}".format(data)) 781 return (None, None) 782 783 parent = None 784 for line in data.split('\n'): 785 if line.strip().startswith('cth_parname'): 786 try: 787 parent = line.split('=')[1].strip() 788 break 789 except: 790 pass 791 792 cmd1 = cmd2 = "{} -c ".format(ctfdump_cmd) 793 if parent == "genunix": 794 if diff_ctf.genunix1 and diff_ctf.genunix2: 795 cmd1 += "-p {} ".format(diff_ctf.genunix1) 796 cmd2 += "-p {} ".format(diff_ctf.genunix2) 797 elif parent is None or (len(parent) > 0 and parent != "(anon)"): 798 error("Unknown CTF Parent: {}".format(parent)) 799 return (None, None) 800 801 cmd1 += f1 802 cmd2 += f2 803 804 return (cmd1, cmd2) 805 806diff_ctf.genunix1 = None 807diff_ctf.genunix2 = None 808 809def diff_elf_section(f1, f2, section, sh_type) : 810 811 t = threading.currentThread() 812 tmpFile1 = tmpDir1 + os.path.basename(f1) + t.getName() 813 tmpFile2 = tmpDir2 + os.path.basename(f2) + t.getName() 814 815 if (sh_type == "SHT_RELA") : # sh_type == SHT_RELA 816 cmd1 = elfdump_cmd + " -r " + f1 + " > " + tmpFile1 817 cmd2 = elfdump_cmd + " -r " + f2 + " > " + tmpFile2 818 elif (section == ".group") : 819 cmd1 = elfdump_cmd + " -g " + f1 + " > " + tmpFile1 820 cmd2 = elfdump_cmd + " -g " + f2 + " > " + tmpFile2 821 elif (section == ".hash") : 822 cmd1 = elfdump_cmd + " -h " + f1 + " > " + tmpFile1 823 cmd2 = elfdump_cmd + " -h " + f2 + " > " + tmpFile2 824 elif (section == ".dynamic") : 825 cmd1 = elfdump_cmd + " -d " + f1 + " > " + tmpFile1 826 cmd2 = elfdump_cmd + " -d " + f2 + " > " + tmpFile2 827 elif (section == ".got") : 828 cmd1 = elfdump_cmd + " -G " + f1 + " > " + tmpFile1 829 cmd2 = elfdump_cmd + " -G " + f2 + " > " + tmpFile2 830 elif (section == ".SUNW_cap") : 831 cmd1 = elfdump_cmd + " -H " + f1 + " > " + tmpFile1 832 cmd2 = elfdump_cmd + " -H " + f2 + " > " + tmpFile2 833 elif (section == ".interp") : 834 cmd1 = elfdump_cmd + " -i " + f1 + " > " + tmpFile1 835 cmd2 = elfdump_cmd + " -i " + f2 + " > " + tmpFile2 836 elif (section == ".symtab" or section == ".dynsym") : 837 cmd1 = (elfdump_cmd + " -s -N " + section + " " + f1 + 838 " > " + tmpFile1) 839 cmd2 = (elfdump_cmd + " -s -N " + section + " " + f2 + 840 " > " + tmpFile2) 841 elif (section in text_sections) : 842 # dis sometimes complains when it hits something it doesn't 843 # know how to disassemble. Just ignore it, as the output 844 # being generated here is human readable, and we've already 845 # correctly flagged the difference. 846 cmd1 = (dis_cmd + " -t " + section + " " + f1 + 847 " 2>/dev/null | grep -v disassembly > " + tmpFile1) 848 cmd2 = (dis_cmd + " -t " + section + " " + f2 + 849 " 2>/dev/null | grep -v disassembly > " + tmpFile2) 850 elif (section == ".SUNW_ctf"): 851 (cmd1, cmd2) = diff_ctf(f1, f2) 852 if not cmd1: 853 return "" 854 cmd1 += " > {}".format(tmpFile1) 855 cmd2 += " > {}".format(tmpFile2) 856 857 else : 858 cmd1 = (elfdump_cmd + " -w " + tmpFile1 + " -N " + 859 section + " " + f1) 860 cmd2 = (elfdump_cmd + " -w " + tmpFile2 + " -N " + 861 section + " " + f2) 862 863 os.system(cmd1) 864 os.system(cmd2) 865 866 data = diffFileData(tmpFile1, tmpFile2) 867 868 # remove temp files as we no longer need them 869 try: 870 os.unlink(tmpFile1) 871 except OSError as e: 872 error("diff_elf_section: unlink failed %s" % e) 873 try: 874 os.unlink(tmpFile2) 875 except OSError as e: 876 error("diff_elf_section: unlink failed %s" % e) 877 878 return (data) 879 880# 881# compare the relevant sections of two ELF binaries 882# and report any differences 883# 884# Returns: 1 if any differenes found 885# 0 if no differences found 886# -1 on error 887# 888 889# Sections deliberately not considered when comparing two ELF 890# binaries. Differences observed in these sections are not considered 891# significant where patch deliverable identification is concerned. 892sections_to_skip = [ ".SUNW_signature", 893 ".comment", 894 ".debug", 895 ".plt", 896 ".rela.bss", 897 ".rela.plt", 898 ".line", 899 ".note", 900 ".compcom", 901 ".SUNW_dof", 902 ] 903 904sections_preferred = [ ".SUNW_ctf", 905 ".rodata.str1.8", 906 ".rodata.str1.1", 907 ".rodata", 908 ".data1", 909 ".data", 910 ".text", 911 ] 912 913# Some sections must always be extracted and diffed to check that there are 914# real differences. 915sections_to_always_diff = [ ".SUNW_ctf" ] 916 917def compareElfs(base, ptch, quiet) : 918 919 global logging 920 921 try: 922 base_header = get_elfheader(base) 923 except: 924 return 925 sections = list(base_header.keys()) 926 sections.sort() 927 928 try: 929 ptch_header = get_elfheader(ptch) 930 except: 931 return 932 e2_only_sections = list(ptch_header.keys()) 933 e2_only_sections.sort() 934 935 e1_only_sections = [] 936 937 fileName = fnFormat(base) 938 939 # Derive the list of ELF sections found only in 940 # either e1 or e2. 941 for sect in sections : 942 if not sect in e2_only_sections : 943 e1_only_sections.append(sect) 944 else : 945 e2_only_sections.remove(sect) 946 947 if len(e1_only_sections) > 0 : 948 if quiet : 949 return 1 950 951 data = "" 952 if logging : 953 slist = "" 954 for sect in e1_only_sections : 955 slist = slist + sect + "\t" 956 data = ("ELF sections found in " + 957 base + " but not in " + ptch + 958 "\n\n" + slist) 959 960 difference(fileName, "ELF", data) 961 return 1 962 963 if len(e2_only_sections) > 0 : 964 if quiet : 965 return 1 966 967 data = "" 968 if logging : 969 slist = "" 970 for sect in e2_only_sections : 971 slist = slist + sect + "\t" 972 data = ("ELF sections found in " + 973 ptch + " but not in " + base + 974 "\n\n" + slist) 975 976 difference(fileName, "ELF", data) 977 return 1 978 979 # Look for preferred sections, and put those at the 980 # top of the list of sections to compare 981 for psect in sections_preferred : 982 if psect in sections : 983 sections.remove(psect) 984 sections.insert(0, psect) 985 986 # Compare ELF sections 987 first_section = True 988 for sect in sections : 989 990 if sect in sections_to_skip : 991 continue 992 993 try: 994 s1 = extract_elf_section(base, sect); 995 except: 996 return 997 998 try: 999 s2 = extract_elf_section(ptch, sect); 1000 except: 1001 return 1002 1003 if len(s1) != len (s2) or s1 != s2: 1004 if not quiet or sect in sections_to_always_diff: 1005 sh_type = base_header[sect] 1006 data = diff_elf_section(base, ptch, 1007 sect, sh_type) 1008 1009 if len(data) == 0: 1010 continue # No differences 1011 1012 if not quiet: 1013 # If all ELF sections are being reported, then 1014 # invoke difference() to flag the file name to 1015 # stdout only once. Any other section 1016 # differences should be logged to the results 1017 # file directly 1018 if not first_section : 1019 log_difference(fileName, 1020 "ELF " + sect, data) 1021 else : 1022 difference(fileName, "ELF " + sect, 1023 data) 1024 1025 if not reportAllSects : 1026 return 1 1027 first_section = False 1028 1029 return 0 1030 1031##### 1032# recursively remove 2 directories 1033# 1034# Used for removal of temporary directory strucures (ignores any errors). 1035# 1036def clearTmpDirs(dir1, dir2) : 1037 1038 if os.path.isdir(dir1) > 0 : 1039 shutil.rmtree(dir1, True) 1040 1041 if os.path.isdir(dir2) > 0 : 1042 shutil.rmtree(dir2, True) 1043 1044 1045##### 1046# Archive object comparison 1047# 1048# Returns 1 if difference detected 1049# 0 if no difference detected 1050# -1 on error 1051# 1052def compareArchives(base, ptch, fileType) : 1053 1054 fileName = fnFormat(base) 1055 t = threading.currentThread() 1056 ArchTmpDir1 = tmpDir1 + os.path.basename(base) + t.getName() 1057 ArchTmpDir2 = tmpDir2 + os.path.basename(base) + t.getName() 1058 1059 # 1060 # Be optimistic and first try a straight file compare 1061 # as it will allow us to finish up quickly. 1062 # 1063 if compareBasic(base, ptch, True, fileType) == 0 : 1064 return 0 1065 1066 try: 1067 os.makedirs(ArchTmpDir1) 1068 except OSError as e: 1069 error("compareArchives: makedir failed %s" % e) 1070 return -1 1071 try: 1072 os.makedirs(ArchTmpDir2) 1073 except OSError as e: 1074 error("compareArchives: makedir failed %s" % e) 1075 return -1 1076 1077 # copy over the objects to the temp areas, and 1078 # unpack them 1079 baseCmd = "cp -fp " + base + " " + ArchTmpDir1 1080 rc, output = getoutput(baseCmd) 1081 if rc != 0: 1082 error(baseCmd + " failed: " + output) 1083 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1084 return -1 1085 1086 ptchCmd = "cp -fp " + ptch + " " + ArchTmpDir2 1087 rc, output = getoutput(ptchCmd) 1088 if rc != 0: 1089 error(ptchCmd + " failed: " + output) 1090 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1091 return -1 1092 1093 bname = fileName.split('/')[-1] 1094 if fileType == "Java Archive" : 1095 baseCmd = ("cd " + ArchTmpDir1 + "; " + "jar xf " + bname + 1096 "; rm -f " + bname + " META-INF/MANIFEST.MF") 1097 ptchCmd = ("cd " + ArchTmpDir2 + "; " + "jar xf " + bname + 1098 "; rm -f " + bname + " META-INF/MANIFEST.MF") 1099 elif fileType == "ELF Object Archive" : 1100 baseCmd = ("cd " + ArchTmpDir1 + "; " + "/usr/ccs/bin/ar x " + 1101 bname + "; rm -f " + bname) 1102 ptchCmd = ("cd " + ArchTmpDir2 + "; " + "/usr/ccs/bin/ar x " + 1103 bname + "; rm -f " + bname) 1104 else : 1105 error("unexpected file type: " + fileType) 1106 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1107 return -1 1108 1109 os.system(baseCmd) 1110 os.system(ptchCmd) 1111 1112 baseFlist = list(findFiles(ArchTmpDir1)) 1113 ptchFlist = list(findFiles(ArchTmpDir2)) 1114 1115 # Trim leading path off base/ptch file lists 1116 flist = [] 1117 for fn in baseFlist : 1118 flist.append(str_prefix_trunc(fn, ArchTmpDir1)) 1119 baseFlist = flist 1120 1121 flist = [] 1122 for fn in ptchFlist : 1123 flist.append(str_prefix_trunc(fn, ArchTmpDir2)) 1124 ptchFlist = flist 1125 1126 for fn in ptchFlist : 1127 if not fn in baseFlist : 1128 difference(fileName, fileType, 1129 fn + " added to " + fileName) 1130 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1131 return 1 1132 1133 for fn in baseFlist : 1134 if not fn in ptchFlist : 1135 difference(fileName, fileType, 1136 fn + " removed from " + fileName) 1137 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1138 return 1 1139 1140 differs = compareOneFile((ArchTmpDir1 + fn), 1141 (ArchTmpDir2 + fn), True) 1142 if differs : 1143 difference(fileName, fileType, 1144 fn + " in " + fileName + " differs") 1145 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1146 return 1 1147 1148 clearTmpDirs(ArchTmpDir1, ArchTmpDir2) 1149 return 0 1150 1151##### 1152# (Basic) file comparison 1153# 1154# Returns 1 if difference detected 1155# 0 if no difference detected 1156# -1 on error 1157# 1158def compareBasic(base, ptch, quiet, fileType) : 1159 1160 fileName = fnFormat(base); 1161 1162 if quiet and os.stat(base)[ST_SIZE] != os.stat(ptch)[ST_SIZE] : 1163 return 1 1164 1165 try: 1166 with open(base, 'rb') as fh: 1167 baseData = fh.read() 1168 except: 1169 error("could not open " + base) 1170 return -1 1171 1172 try: 1173 with open(ptch, 'rb') as fh: 1174 ptchData = fh.read() 1175 except: 1176 error("could not open " + ptch) 1177 return -1 1178 1179 if quiet : 1180 if baseData != ptchData : 1181 return 1 1182 else : 1183 if len(baseData) != len(ptchData) or baseData != ptchData : 1184 diffs = diffFileData(base, ptch) 1185 difference(fileName, fileType, diffs) 1186 return 1 1187 return 0 1188 1189 1190##### 1191# Compare two objects by producing a data dump from 1192# each object, and then comparing the dump data 1193# 1194# Returns: 1 if a difference is detected 1195# 0 if no difference detected 1196# -1 upon error 1197# 1198def compareByDumping(base, ptch, quiet, fileType) : 1199 1200 fileName = fnFormat(base); 1201 t = threading.currentThread() 1202 tmpFile1 = tmpDir1 + os.path.basename(base) + t.getName() 1203 tmpFile2 = tmpDir2 + os.path.basename(ptch) + t.getName() 1204 1205 if fileType == "Lint Library" : 1206 baseCmd = (lintdump_cmd + " -ir " + base + 1207 " | egrep -v '(LINTOBJ|LINTMOD):'" + 1208 " | grep -v PASS[1-3]:" + 1209 " > " + tmpFile1) 1210 ptchCmd = (lintdump_cmd + " -ir " + ptch + 1211 " | egrep -v '(LINTOBJ|LINTMOD):'" + 1212 " | grep -v PASS[1-3]:" + 1213 " > " + tmpFile2) 1214 elif fileType == "Sqlite Database" : 1215 baseCmd = ("echo .dump | " + sqlite_cmd + base + " > " + 1216 tmpFile1) 1217 ptchCmd = ("echo .dump | " + sqlite_cmd + ptch + " > " + 1218 tmpFile2) 1219 1220 os.system(baseCmd) 1221 os.system(ptchCmd) 1222 1223 try: 1224 with open(tmpFile1, 'rb') as fh: 1225 baseData = fh.read() 1226 except: 1227 error("could not open: " + tmpFile1) 1228 return 1229 1230 try: 1231 with open(tmpFile2, 'rb') as fh: 1232 ptchData = fh.read() 1233 except: 1234 error("could not open: " + tmpFile2) 1235 return 1236 1237 ret = 0 1238 1239 if len(baseData) != len(ptchData) or baseData != ptchData : 1240 if not quiet : 1241 data = diffFileData(tmpFile1, tmpFile2); 1242 ret = 1 1243 1244 # Remove the temporary files now. 1245 try: 1246 os.unlink(tmpFile1) 1247 except OSError as e: 1248 error("compareByDumping: unlink failed %s" % e) 1249 try: 1250 os.unlink(tmpFile2) 1251 except OSError as e: 1252 error("compareByDumping: unlink failed %s" % e) 1253 1254 return ret 1255 1256##### 1257# 1258# SIGINT signal handler. Changes thread control variable to tell the threads 1259# to finish their current job and exit. 1260# 1261def discontinue_processing(signl, frme): 1262 global keep_processing 1263 1264 print("Caught Ctrl-C, stopping the threads", file=sys.stderr) 1265 keep_processing = False 1266 1267 return 0 1268 1269##### 1270# 1271# worker thread for changedFiles processing 1272# 1273class workerThread(threading.Thread) : 1274 def run(self): 1275 global wset_lock 1276 global changedFiles 1277 global baseRoot 1278 global ptchRoot 1279 global keep_processing 1280 1281 while (keep_processing) : 1282 # grab the lock to changedFiles and remove one member 1283 # and process it 1284 wset_lock.acquire() 1285 try : 1286 fn = changedFiles.pop() 1287 except IndexError : 1288 # there is nothing more to do 1289 wset_lock.release() 1290 return 1291 wset_lock.release() 1292 1293 base = baseRoot + fn 1294 ptch = ptchRoot + fn 1295 1296 compareOneFile(base, ptch, False) 1297 1298 1299##### 1300# Compare two objects. Detect type changes. 1301# Vector off to the appropriate type specific 1302# compare routine based on the type. 1303# 1304def compareOneFile(base, ptch, quiet) : 1305 1306 # Verify the file types. 1307 # If they are different, indicate this and move on 1308 btype = getTheFileType(base) 1309 ptype = getTheFileType(ptch) 1310 1311 if btype == 'Error' or ptype == 'Error' : 1312 return -1 1313 1314 fileName = fnFormat(base) 1315 1316 if (btype != ptype) : 1317 if not quiet : 1318 difference(fileName, "file type", 1319 btype + " to " + ptype) 1320 return 1 1321 else : 1322 fileType = btype 1323 1324 if (fileType == 'ELF') : 1325 return compareElfs(base, ptch, quiet) 1326 1327 elif (fileType == 'Java Archive' or fileType == 'ELF Object Archive') : 1328 return compareArchives(base, ptch, fileType) 1329 1330 elif (fileType == 'HTML') : 1331 return compareBasic(base, ptch, quiet, fileType) 1332 1333 elif ( fileType == 'Lint Library' ) : 1334 return compareByDumping(base, ptch, quiet, fileType) 1335 1336 elif ( fileType == 'Sqlite Database' ) : 1337 return compareByDumping(base, ptch, quiet, fileType) 1338 1339 else : 1340 # it has to be some variety of text file 1341 return compareBasic(base, ptch, quiet, fileType) 1342 1343# Cleanup and self-terminate 1344def cleanup(ret) : 1345 1346 debug("Performing cleanup (" + str(ret) + ")") 1347 if os.path.isdir(tmpDir1) > 0 : 1348 shutil.rmtree(tmpDir1) 1349 1350 if os.path.isdir(tmpDir2) > 0 : 1351 shutil.rmtree(tmpDir2) 1352 1353 if logging : 1354 log.close() 1355 1356 sys.exit(ret) 1357 1358def main() : 1359 1360 # Log file handle 1361 global log 1362 1363 # Globals relating to command line options 1364 global logging, vdiffs, reportAllSects 1365 1366 # Named temporary files / directories 1367 global tmpDir1, tmpDir2 1368 1369 # Command paths 1370 global lintdump_cmd, elfdump_cmd, dump_cmd, dis_cmd, od_cmd, \ 1371 diff_cmd, sqlite_cmd, ctfdump_cmd 1372 1373 # Default search path 1374 global wsdiff_path 1375 1376 # Essentially "uname -p" 1377 global arch 1378 1379 # changed files for worker thread processing 1380 global changedFiles 1381 global baseRoot 1382 global ptchRoot 1383 1384 # Sort the list of files from a temporary file 1385 global o_sorted 1386 global differentFiles 1387 1388 # Debugging indicator 1389 global debugon 1390 1391 # Some globals need to be initialized 1392 debugon = logging = vdiffs = reportAllSects = o_sorted = False 1393 1394 # Process command line arguments 1395 # Return values are returned from args() in alpha order 1396 # (Yes, python functions can return multiple values (ewww)) 1397 # Note that args() also set the globals: 1398 # logging to True if verbose logging (to a file) was enabled 1399 # vdiffs to True if logged differences aren't to be truncated 1400 # reportAllSects to True if all ELF section differences are to 1401 # be reported 1402 # 1403 baseRoot, fileNamesFile, localTools, ptchRoot, results = args() 1404 1405 # 1406 # Set up the results/log file 1407 # 1408 if logging : 1409 try: 1410 log = open(results, "w") 1411 except: 1412 logging = False 1413 error("failed to open log file: " + log) 1414 sys.exit(1) 1415 1416 dateTimeStr= "# %04d-%02d-%02d at %02d:%02d:%02d" % \ 1417 time.localtime()[:6] 1418 v_info("# This file was produced by wsdiff") 1419 v_info(dateTimeStr) 1420 1421 # Changed files (used only for the sorted case) 1422 if o_sorted : 1423 differentFiles = [] 1424 1425 # 1426 # Build paths to the tools required tools 1427 # 1428 # Try to look for tools in $SRC/tools if the "-t" option 1429 # was specified 1430 # 1431 rc, arch = getoutput("uname -p") 1432 arch = arch.rstrip() 1433 if localTools : 1434 try: 1435 src = os.environ['SRC'] 1436 except: 1437 error("-t specified, but $SRC not set. " + 1438 "Cannot find $SRC/tools") 1439 src = "" 1440 if len(src) > 0 : 1441 wsdiff_path.insert(0, 1442 src + "/tools/proto/opt/onbld/bin") 1443 1444 lintdump_cmd = find_tool("lintdump") 1445 elfdump_cmd = find_tool("elfdump") 1446 dump_cmd = find_tool("dump") 1447 od_cmd = find_tool("od") 1448 dis_cmd = find_tool("dis") 1449 diff_cmd = find_tool("diff") 1450 sqlite_cmd = find_tool("sqlite") 1451 ctfdump_cmd = find_tool("ctfdump") 1452 1453 # 1454 # Set resource limit for number of open files as high as possible. 1455 # This might get handy with big number of threads. 1456 # 1457 (nofile_soft, nofile_hard) = resource.getrlimit(resource.RLIMIT_NOFILE) 1458 try: 1459 resource.setrlimit(resource.RLIMIT_NOFILE, 1460 (nofile_hard, nofile_hard)) 1461 except: 1462 error("cannot set resource limits for number of open files") 1463 sys.exit(1) 1464 1465 # 1466 # validate the base and patch paths 1467 # 1468 if baseRoot[-1] != '/' : 1469 baseRoot += '/' 1470 1471 if ptchRoot[-1] != '/' : 1472 ptchRoot += '/' 1473 1474 if not os.path.exists(baseRoot) : 1475 error("old proto area: " + baseRoot + " does not exist") 1476 sys.exit(1) 1477 1478 if not os.path.exists(ptchRoot) : 1479 error("new proto area: " + ptchRoot + " does not exist") 1480 sys.exit(1) 1481 1482 # 1483 # log some information identifying the run 1484 # 1485 v_info("Old proto area: " + baseRoot) 1486 v_info("New proto area: " + ptchRoot) 1487 v_info("Results file: " + results + "\n") 1488 1489 # 1490 # Set up the temporary directories / files 1491 # Could use python's tmpdir routines, but these should 1492 # be easier to identify / keep around for debugging 1493 pid = os.getpid() 1494 tmpDir1 = "/tmp/wsdiff_tmp1_" + str(pid) + "/" 1495 tmpDir2 = "/tmp/wsdiff_tmp2_" + str(pid) + "/" 1496 try: 1497 os.makedirs(tmpDir1) 1498 except OSError as e: 1499 error("main: makedir failed %s" % e) 1500 try: 1501 os.makedirs(tmpDir2) 1502 except OSError as e: 1503 error("main: makedir failed %s" % e) 1504 1505 # Derive a catalog of new, deleted, and to-be-compared objects 1506 # either from the specified base and patch proto areas, or from 1507 # from an input file list 1508 newOrDeleted = False 1509 1510 if fileNamesFile != "" : 1511 changedFiles, newFiles, deletedFiles = \ 1512 flistCatalog(baseRoot, ptchRoot, fileNamesFile) 1513 else : 1514 changedFiles, newFiles, deletedFiles = \ 1515 protoCatalog(baseRoot, ptchRoot) 1516 1517 if len(newFiles) > 0 : 1518 newOrDeleted = True 1519 info("\nNew objects found: ") 1520 1521 if o_sorted : 1522 newFiles.sort() 1523 for fn in newFiles : 1524 info(fnFormat(fn)) 1525 1526 if len(deletedFiles) > 0 : 1527 newOrDeleted = True 1528 info("\nObjects removed: ") 1529 1530 if o_sorted : 1531 deletedFiles.sort() 1532 for fn in deletedFiles : 1533 info(fnFormat(fn)) 1534 1535 if newOrDeleted : 1536 info("\nChanged objects: ") 1537 if o_sorted : 1538 debug("The list will appear after the processing is done") 1539 1540 # Here's where all the heavy lifting happens 1541 # Perform a comparison on each object appearing in 1542 # both proto areas. compareOneFile will examine the 1543 # file types of each object, and will vector off to 1544 # the appropriate comparison routine, where the compare 1545 # will happen, and any differences will be reported / logged 1546 1547 # determine maximum number of worker threads by using 1548 # DMAKE_MAX_JOBS environment variable set by nightly(1) 1549 # or get number of CPUs in the system 1550 try: 1551 max_threads = int(os.environ['DMAKE_MAX_JOBS']) 1552 except: 1553 max_threads = os.sysconf("SC_NPROCESSORS_ONLN") 1554 # If we cannot get number of online CPUs in the system 1555 # run unparallelized otherwise bump the number up 20% 1556 # to achieve best results. 1557 if max_threads == -1 : 1558 max_threads = 1 1559 else : 1560 max_threads += int(max_threads/5) 1561 1562 # Set signal handler to attempt graceful exit 1563 debug("Setting signal handler") 1564 signal.signal( signal.SIGINT, discontinue_processing ) 1565 1566 # Create and unleash the threads 1567 # Only at most max_threads must be running at any moment 1568 mythreads = [] 1569 debug("Spawning " + str(max_threads) + " threads"); 1570 for i in range(max_threads) : 1571 thread = workerThread() 1572 mythreads.append(thread) 1573 mythreads[i].start() 1574 1575 # Wait for the threads to finish and do cleanup if interrupted 1576 debug("Waiting for the threads to finish") 1577 while True: 1578 if not True in [thread.is_alive() for thread in mythreads]: 1579 break 1580 else: 1581 # Some threads are still going 1582 time.sleep(1) 1583 1584 # Interrupted by SIGINT 1585 if keep_processing == False : 1586 cleanup(1) 1587 1588 # If the list of differences was sorted it is stored in an array 1589 if o_sorted : 1590 differentFiles.sort() 1591 for f in differentFiles : 1592 info(fnFormat(f)) 1593 1594 # We're done, cleanup. 1595 cleanup(0) 1596 1597if __name__ == '__main__' : 1598 try: 1599 main() 1600 except KeyboardInterrupt : 1601 cleanup(1); 1602 1603