cppcheck/tools/donate-cpu.py
Sebastian fe6f4193fd donate-cpu.py: Fix Python3 compatibility problem with f.read().decode() (#1507)
With Python3 f.read() directly returns a string object that has no
decode() function. As a workaround AttributeError exceptions during
calling the decode() function are ignored and the data read from the file
is left unchanged.
With Python2 calling the decode() function is necessary and still done.
2018-12-07 06:51:07 +01:00

419 lines
14 KiB
Python

# Donate CPU
#
# A script a user can run to donate CPU to cppcheck project
#
# Syntax: donate-cpu.py [-jN] [--package=url] [--stop-time=HH:MM] [--work-path=path]
# -jN Use N threads in compilation/analysis. Default is 1.
# --package=url Check a specific package and then stop. Can be useful if you want to reproduce some warning/crash/exception/etc..
# --stop-time=HH:MM Stop analysis when time has passed. Default is that you must terminate the script.
# --work-path=path Work folder path. Default path is cppcheck-donate-cpu-workfolder in your home folder.
#
# What this script does:
# 1. Check requirements
# 2. Pull & compile Cppcheck
# 3. Select a package
# 4. Download package
# 5. Analyze source code
# 6. Upload results
# 7. Repeat from step 2
#
# Quick start: just run this script without any arguments
import shutil
import os
import subprocess
import sys
import socket
import time
import re
import tarfile
def checkRequirements():
result = True
for app in ['g++', 'git', 'make', 'wget']:
try:
subprocess.call([app, '--version'])
except OSError:
print(app + ' is required')
result = False
return result
def getCppcheck(cppcheckPath):
print('Get Cppcheck..')
for i in range(5):
if os.path.exists(cppcheckPath):
os.chdir(cppcheckPath)
subprocess.call(['git', 'checkout', '-f'])
subprocess.call(['git', 'pull'])
else:
subprocess.call(['git', 'clone', 'https://github.com/danmar/cppcheck.git', cppcheckPath])
if not os.path.exists(cppcheckPath):
print('Failed to clone, will try again in 10 minutes..')
time.sleep(600)
continue
time.sleep(2)
return True
return False
def compile_version(workPath, jobs, version):
if os.path.isfile(workPath + '/' + version + '/cppcheck'):
return True
os.chdir(workPath + '/cppcheck')
subprocess.call(['git', 'checkout', version])
subprocess.call(['make', 'clean'])
subprocess.call(['make', jobs, 'SRCDIR=build', 'CXXFLAGS=-O2'])
if os.path.isfile(workPath + '/cppcheck/cppcheck'):
os.mkdir(workpath + '/' + version)
destPath = workpath + '/' + version + '/'
subprocess.call(['cp', '-R', workPath + '/cppcheck/cfg', destPath])
subprocess.call(['cp', 'cppcheck', destPath])
subprocess.call(['git', 'checkout', 'master'])
try:
subprocess.call([workPath + '/' + version + '/cppcheck', '--version'])
except OSError:
return False
return True
def compile(cppcheckPath, jobs):
print('Compiling Cppcheck..')
try:
os.chdir(cppcheckPath)
subprocess.call(['make', jobs, 'SRCDIR=build', 'CXXFLAGS=-O2'])
subprocess.call([cppcheckPath + '/cppcheck', '--version'])
except OSError:
return False
return True
def getCppcheckVersions():
print('Connecting to server to get Cppcheck versions..')
package = None
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('cppcheck.osuosl.org', 8000)
try:
sock.connect(server_address)
sock.send(b'GetCppcheckVersions\n')
versions = sock.recv(256)
except socket.error:
return ['head', '1.85']
sock.close()
return versions.decode('utf-8').split()
def getPackage():
print('Connecting to server to get assigned work..')
package = None
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('cppcheck.osuosl.org', 8000)
try:
sock.connect(server_address)
sock.send(b'get\n')
package = sock.recv(256)
except socket.error:
package = ''
sock.close()
return package.decode('utf-8')
def handleRemoveReadonly(func, path, exc):
import stat
if not os.access(path, os.W_OK):
# Is the error an access error ?
os.chmod(path, stat.S_IWUSR)
func(path)
def removeTree(folderName):
if not os.path.exists(folderName):
return
count = 5
while count > 0:
count -= 1
try:
shutil.rmtree(folderName, onerror=handleRemoveReadonly)
break
except OSError as err:
time.sleep(30)
if count == 0:
print('Failed to cleanup {}: {}'.format(folderName, err))
sys.exit(1)
def wget(url, destfile):
if os.path.exists(destfile):
if os.path.isfile(destfile):
os.remove(destfile)
else:
print('Error: ' + destfile + ' exists but it is not a file! Please check the path and delete it manually.')
sys.exit(1)
subprocess.call(
['wget', '--tries=10', '--timeout=300', '-O', destfile, url])
if os.path.isfile(destfile):
return True
print('Sleep for 10 seconds..')
time.sleep(10)
return False
def downloadPackage(workPath, package):
print('Download package ' + package)
destfile = workPath + '/temp.tgz'
if not wget(package, destfile):
if not wget(package, destfile):
return None
return destfile
def unpackPackage(workPath, tgz):
print('Unpacking..')
tempPath = workPath + '/temp'
removeTree(tempPath)
os.mkdir(tempPath)
os.chdir(tempPath)
if tarfile.is_tarfile(tgz):
tf = tarfile.open(tgz)
for member in tf:
if member.name.startswith(('/', '..')):
# Skip dangerous file names
continue
elif member.name.lower().endswith(('.c', '.cl', '.cpp', '.cxx', '.cc', '.c++', '.h', '.hpp', '.hxx', '.hh', '.tpp', '.txx')):
try:
tf.extract(member.name)
except OSError:
pass
except AttributeError:
pass
tf.close()
os.chdir(workPath)
def hasInclude(path, inc):
for root, _, files in os.walk(path):
for name in files:
filename = os.path.join(root, name)
try:
f = open(filename, 'rt')
filedata = f.read()
try:
# Python2 needs to decode the data first
filedata = filedata.decode(encoding='utf-8', errors='ignore')
except AttributeError:
# Python3 directly reads the data into a string object that has no decode()
pass
f.close()
if filedata.find('\n#include ' + inc) >= 0:
return True
except IOError:
pass
return False
def scanPackage(workPath, cppcheck, jobs):
print('Analyze..')
os.chdir(workPath)
libraries = ' --library=posix'
if hasInclude('temp', '<wx/string.h>'):
libraries += ' --library=wxwidgets'
if hasInclude('temp', '<QString>'):
libraries += ' --library=qt'
cmd = 'nice ' + cppcheck + ' ' + jobs + libraries + ' -D__GCC__ --inconclusive --enable=style --platform=unix64 --template=daca2 -rp=temp temp'
print(cmd)
startTime = time.time()
p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
comm = p.communicate()
stopTime = time.time()
stdout = comm[0].decode(encoding='utf-8', errors='ignore')
stderr = comm[1].decode(encoding='utf-8', errors='ignore')
if p.returncode != 0 and 'cppcheck: error: could not find or open any of the paths given.' not in stdout:
# Crash!
print('Crash!')
return -1, '', -1
if stderr.find('Internal error: Child process crashed with signal 11 [cppcheckError]') > 0:
# Crash!
print('Crash!')
return -1, '', -1
elapsedTime = stopTime - startTime
count = 0
for line in stderr.split('\n'):
if re.match(r'.*:[0-9]+:.*\]$', line):
count += 1
print('Number of issues: ' + str(count))
return count, stderr, elapsedTime
def splitResults(results):
ret = []
w = None
for line in results.split('\n'):
if line.endswith(']') and re.search(r': (error|warning|style|performance|portability|information|debug):', line):
if w is not None:
ret.append(w.strip())
w = ''
if w is not None:
w += ' ' * 5 + line + '\n'
if w is not None:
ret.append(w.strip())
return ret
def diffResults(workPath, ver1, results1, ver2, results2):
print('Diff results..')
ret = ''
r1 = sorted(splitResults(results1))
r2 = sorted(splitResults(results2))
i1 = 0
i2 = 0
while i1 < len(r1) and i2 < len(r2):
if r1[i1] == r2[i2]:
i1 += 1
i2 += 1
elif r1[i1] < r2[i2]:
ret += ver1 + ' ' + r1[i1] + '\n'
i1 += 1
else:
ret += ver2 + ' ' + r2[i2] + '\n'
i2 += 1
while i1 < len(r1):
ret += ver1 + ' ' + r1[i1] + '\n'
i1 += 1
while i2 < len(r2):
ret += ver2 + ' ' + r2[i2] + '\n'
i2 += 1
return ret
def sendAll(connection, data):
bytes = data.encode('ascii', 'ignore')
while bytes:
num = connection.send(bytes)
if num < len(bytes):
bytes = bytes[num:]
else:
bytes = None
def uploadResults(package, results):
print('Uploading results..')
for retry in range(4):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('cppcheck.osuosl.org', 8000)
sock.connect(server_address)
sendAll(sock, 'write\n' + package + '\n' + results + '\nDONE')
sock.close()
return True
except socket.error:
print('Upload failed, retry in 60 seconds')
time.sleep(30)
pass
return False
jobs = '-j1'
stopTime = None
workpath = os.path.expanduser('~/cppcheck-donate-cpu-workfolder')
packageUrl = None
for arg in sys.argv[1:]:
# --stop-time=12:00 => run until ~12:00 and then stop
if arg.startswith('--stop-time='):
stopTime = arg[-5:]
print('Stop time:' + stopTime)
elif arg.startswith('-j'):
jobs = arg
print('Jobs:' + jobs[2:])
elif arg.startswith('--package='):
packageUrl = arg[arg.find('=')+1:]
print('Package:' + packageUrl)
elif arg.startswith('--work-path='):
workpath = arg[arg.find('=')+1:]
print('workpath:' + workpath)
if not os.path.exists(workpath):
print('work path does not exist!')
sys.exit(1)
elif arg == '--help':
print('Donate CPU to Cppcheck project')
print('')
print('Syntax: donate-cpu.py [-jN] [--stop-time=HH:MM] [--work-path=path]')
print(' -jN Use N threads in compilation/analysis. Default is 1.')
print(' --stop-time=HH:MM Stop analysis when time has passed. Default is that you must terminate the script.')
print(' --work-path=path Work folder path. Default path is ' + workpath)
print('')
print('Quick start: just run this script without any arguments')
sys.exit(0)
else:
print('Unhandled argument: ' + arg)
sys.exit(1)
print('Thank you!')
if not checkRequirements():
sys.exit(1)
if not os.path.exists(workpath):
os.mkdir(workpath)
cppcheckPath = workpath + '/cppcheck'
while True:
if stopTime:
print('stopTime:' + stopTime + '. Time:' + time.strftime('%H:%M') + '.')
if stopTime < time.strftime('%H:%M'):
print('Stopping. Thank you!')
sys.exit(0)
if not getCppcheck(cppcheckPath):
print('Failed to clone Cppcheck, retry later')
sys.exit(1)
cppcheckVersions = getCppcheckVersions()
for ver in cppcheckVersions:
if ver == 'head':
if compile(cppcheckPath, jobs) == False:
print('Failed to compile Cppcheck, retry later')
sys.exit(1)
elif compile_version(workpath, jobs, ver) == False:
print('Failed to compile Cppcheck-{}, retry later'.format(ver))
sys.exit(1)
if packageUrl:
package = packageUrl
else:
package = getPackage()
while len(package) == 0:
print("network or server might be temporarily down.. will try again in 30 seconds..")
time.sleep(30)
package = getPackage()
tgz = downloadPackage(workpath, package)
unpackPackage(workpath, tgz)
crash = False
count = ''
elapsedTime = ''
resultsToDiff = []
for ver in cppcheckVersions:
if ver == 'head':
cppcheck = 'cppcheck/cppcheck'
else:
cppcheck = ver + '/cppcheck'
c,errout,t = scanPackage(workpath, cppcheck, jobs)
if c < 0:
crash = True
count += ' Crash!'
else:
count += ' ' + str(c)
elapsedTime += " {:.1f}".format(t)
resultsToDiff.append(errout)
if not crash and len(resultsToDiff[0]) + len(resultsToDiff[1]) == 0:
print('No results')
continue
output = 'cppcheck: ' + ' '.join(cppcheckVersions) + '\n'
output += 'count:' + count + '\n'
output += 'elapsed-time:' + elapsedTime + '\n'
if 'head' in cppcheckVersions:
output += 'head results:\n' + resultsToDiff[cppcheckVersions.index('head')]
if not crash:
output += 'diff:\n' + diffResults(workpath, cppcheckVersions[0], resultsToDiff[0], cppcheckVersions[1], resultsToDiff[1]) + '\n'
if packageUrl:
print('=========================================================')
print(output)
print('=========================================================')
break
uploadResults(package, output)
print('Results have been uploaded')
print('Sleep 5 seconds..')
time.sleep(5)