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