GUI: Canceling verification now also kills back-end thread.

Previously, pressing 'cancel' in the verification window would leave a dangling scyther backend process.
Now the process gets correctly killed.

The following changes enable this:
- External processes are no longer invoked through the shell (otherwise they are subprocesses of the shell and cannot be reliably killed cross-platform).
- The 'safeCommand' procedure now has a hook for passing opened Popen objects.
- The GUI stores and kills the Popen objects on cancel or window close.

To do: an alternative interface for this in 'safeCommand' could expose a 'killMe' method through a callback; this might be cleaner in the long term.
This commit is contained in:
Cas Cremers 2013-06-30 23:14:28 +02:00
parent 6473aba398
commit fe364fbe9d
3 changed files with 62 additions and 35 deletions

View File

@ -56,8 +56,21 @@ class ScytherThread(threading.Thread):
self.options = options self.options = options
self.callback = callback self.callback = callback
self.mode = mode self.mode = mode
self.popenList = []
threading.Thread.__init__ ( self ) threading.Thread.__init__ ( self )
def storePopen(self,p):
self.popenList.append(p)
def cleanExit(self):
# Cleanup of spawned processes
for index,p in enumerate(self.popenList):
try:
p.kill()
except:
pass
self.popenList = []
def run(self): def run(self):
(scyther, claims, summary) = self.claimResults() (scyther, claims, summary) = self.claimResults()
@ -100,7 +113,7 @@ class ScytherThread(threading.Thread):
# verification start # verification start
try: try:
claims = scyther.verify() claims = scyther.verify(storePopen=self.storePopen)
except Scyther.Error.ScytherError, el: except Scyther.Error.ScytherError, el:
claims = None claims = None
pass pass
@ -185,6 +198,9 @@ class VerificationWindow(wx.Dialog):
self.SetSizer(sizer) self.SetSizer(sizer)
sizer.Fit(self) sizer.Fit(self)
self.Center()
self.Show(True)
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
class ErrorWindow(wx.Dialog): class ErrorWindow(wx.Dialog):
@ -398,7 +414,6 @@ class ResultWindow(wx.Frame):
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
class ScytherRun(object): class ScytherRun(object):
def __init__(self,mainwin,mode,spdl,errorcallback=None): def __init__(self,mainwin,mode,spdl,errorcallback=None):
@ -409,8 +424,23 @@ class ScytherRun(object):
self.verified = False self.verified = False
self.options = mainwin.settings.ScytherArguments(mode) self.options = mainwin.settings.ScytherArguments(mode)
self.errorcallback=errorcallback self.errorcallback=errorcallback
self.SThread = None
self.main() self.main()
def closer(self,ev):
# Triggered when the window is closed/verification cancelled
t = self.SThread
if t != None:
self.SThread = None
t.cleanExit()
try:
self.verifywin.Destroy()
except:
pass
self.verifywin = None
ev.Skip()
def main(self): def main(self):
""" """
Start process Start process
@ -422,8 +452,6 @@ class ScytherRun(object):
# if the thread terminames, it should close the window normally, and we end up here as well. # if the thread terminames, it should close the window normally, and we end up here as well.
#val = self.verifywin.ShowModal() #val = self.verifywin.ShowModal()
self.verifywin = VerificationWindow(self.mainwin,title) self.verifywin = VerificationWindow(self.mainwin,title)
self.verifywin.Center()
self.verifywin.Show(True)
# Check sanity of Scyther thing here (as opposed to the thread) # Check sanity of Scyther thing here (as opposed to the thread)
# which makes error reporting somewhat easier # which makes error reporting somewhat easier
@ -435,10 +463,13 @@ class ScytherRun(object):
Error.ShowAndExit(text) Error.ShowAndExit(text)
# start the thread # start the thread
self.verifywin.SetCursor(wx.StockCursor(wx.CURSOR_WAIT)) self.verifywin.SetCursor(wx.StockCursor(wx.CURSOR_WAIT))
t = ScytherThread(self.spdl, self.options, self.verificationDone, self.mode) self.verifywin.Bind(wx.EVT_CLOSE, self.closer)
t.start() self.verifywin.Bind(wx.EVT_WINDOW_DESTROY, self.closer)
self.verifywin.Bind(wx.EVT_BUTTON, self.closer, id=wx.ID_CANCEL)
self.SThread = ScytherThread(self.spdl, self.options, self.verificationDone, self.mode)
self.SThread.start()
# after verification, we proceed to the callback below... # after verification, we proceed to the callback below...
@ -447,6 +478,9 @@ class ScytherRun(object):
This is where we end up after a callback from the thread, stating that verification succeeded. This is where we end up after a callback from the thread, stating that verification succeeded.
""" """
if self.verifywin == None:
return
self.scyther = scyther self.scyther = scyther
self.claims = claims self.claims = claims
self.summary = summary self.summary = summary

View File

@ -26,6 +26,7 @@
""" Import externals """ """ Import externals """
import sys import sys
import os.path import os.path
import shlex
try: try:
from subprocess import Popen,PIPE from subprocess import Popen,PIPE
except: except:
@ -89,35 +90,26 @@ def mypath(file):
basedir = os.path.split(cmd_file)[0] basedir = os.path.split(cmd_file)[0]
return os.path.join(basedir,file) return os.path.join(basedir,file)
def getShell(): def safeCommandOutput(cmd, storePopen=None):
"""
Determine if we want a shell for Popen
"""
if sys.platform.startswith("win"):
shell=False
else:
# Needed to handle the string input correctly (as opposed to a sequence where the first element is the executable)
# This is not needed on Windows, where it has a different effect altogether.
# See http://docs.python.org/library/subprocess.html?highlight=subprocess#module-subprocess
shell=True
return shell
def safeCommandOutput(cmd):
""" Execute a command and return (sts,sout,serr). """ Execute a command and return (sts,sout,serr).
Meant for short outputs, as output is stored in memory and Meant for short outputs, as output is stored in memory and
not written to a file. not written to a file.
""" """
p = Popen(cmd, shell=getShell(), stdout=PIPE, stderr=PIPE) p = Popen(shlex.split(cmd), shell=False, stdout=PIPE, stderr=PIPE)
if storePopen != None:
storePopen(p)
(sout,serr) = p.communicate() (sout,serr) = p.communicate()
return (p.returncode,sout,serr) return (p.returncode,sout,serr)
def safeCommand(cmd): def safeCommand(cmd, storePopen=None):
""" Execute a command with some arguments. Safe cross-platform """ Execute a command with some arguments. Safe cross-platform
version, I hope. """ version, I hope. """
try: try:
p = Popen(cmd, shell=getShell()) p = Popen(shlex.split(cmd), shell=False)
if storePopen != None:
storePopen(p)
sts = p.wait() sts = p.wait()
except KeyboardInterrupt, EnvironmentError: except KeyboardInterrupt, EnvironmentError:
raise raise

View File

@ -265,7 +265,7 @@ class Scyther(object):
for arg in arglist: for arg in arglist:
self.options += " %s" % (arg) self.options += " %s" % (arg)
def doScytherCommand(self, spdl, args, checkKnown=False): def doScytherCommand(self, spdl, args, checkKnown=False, storePopen=None):
""" """
Cached version of the 'real' below Cached version of the 'real' below
@ -290,7 +290,7 @@ class Scyther(object):
return False return False
else: else:
# Need to compute # Need to compute
return self.doScytherCommandReal(spdl,args) return self.doScytherCommandReal(spdl,args, storePopen=storePopen)
# Apparently we are supporsed to be able to use the cache # Apparently we are supporsed to be able to use the cache
m = hashlib.sha256() m = hashlib.sha256()
@ -347,7 +347,7 @@ class Scyther(object):
# We were only checking, abort # We were only checking, abort
return False return False
(out,err) = self.doScytherCommandReal(spdl,args) (out,err) = self.doScytherCommandReal(spdl,args, storePopen=storePopen)
try: try:
# Try to store result in cache # Try to store result in cache
@ -366,13 +366,14 @@ class Scyther(object):
return (out,err) return (out,err)
def doScytherCommandReal(self, spdl, args): def doScytherCommandReal(self, spdl, args, storePopen=None):
""" """
Run Scyther backend on the input Run Scyther backend on the input
Arguments: Arguments:
spdl -- string describing the spdl text spdl -- string describing the spdl text
args -- arguments for the command-line args -- arguments for the command-line
storePopen -- callback function to register Popen objects (used for process kill by other threads)
Returns: Returns:
(output,errors) (output,errors)
output -- string which is the real output output -- string which is the real output
@ -415,7 +416,7 @@ class Scyther(object):
##print self.cmd ##print self.cmd
# Start the process # Start the process
safeCommand(self.cmd) safeCommand(self.cmd, storePopen=storePopen)
# reseek # reseek
fhe = os.fdopen(fde) fhe = os.fdopen(fde)
@ -437,7 +438,7 @@ class Scyther(object):
""" Sanitize some of the input """ """ Sanitize some of the input """
self.options = EnsureString(self.options) self.options = EnsureString(self.options)
def verify(self,extraoptions=None,checkKnown=False): def verify(self,extraoptions=None,checkKnown=False,storePopen=None):
""" Should return a list of results """ """ Should return a list of results """
""" If checkKnown == True, we do not call Scyther, but just check the cache, and return True iff the result is in the cache """ """ If checkKnown == True, we do not call Scyther, but just check the cache, and return True iff the result is in the cache """
@ -455,10 +456,10 @@ class Scyther(object):
# Are we only checking the cache? # Are we only checking the cache?
if checkKnown == True: if checkKnown == True:
return self.doScytherCommand(self.spdl, args, checkKnown=checkKnown) return self.doScytherCommand(self.spdl, args, checkKnown=checkKnown, storePopen=storePopen)
# execute # execute
(output,errors) = self.doScytherCommand(self.spdl, args) (output,errors) = self.doScytherCommand(self.spdl, args, storePopen=storePopen)
self.run = True self.run = True
# process errors # process errors
@ -504,7 +505,7 @@ class Scyther(object):
else: else:
return self.output return self.output
def verifyOne(self,cl=None,checkKnown=False): def verifyOne(self,cl=None,checkKnown=False,storePopen=None):
""" """
Verify just a single claim with an ID retrieved from the Verify just a single claim with an ID retrieved from the
procedure below, 'scanClaims', or a full claim object procedure below, 'scanClaims', or a full claim object
@ -515,10 +516,10 @@ class Scyther(object):
# We accept either a claim or a claim id # We accept either a claim or a claim id
if isinstance(cl,Claim.Claim): if isinstance(cl,Claim.Claim):
cl = cl.id cl = cl.id
return self.verify("--filter=%s" % cl, checkKnown=checkKnown) return self.verify("--filter=%s" % cl, checkKnown=checkKnown,storePopen=storePopen)
else: else:
# If no claim, then its just normal verification # If no claim, then its just normal verification
return self.verify(checkKnown=checkKnown) return self.verify(checkKnown=checkKnown,storePopen=storePopen)
def scanClaims(self): def scanClaims(self):
""" """