mirror of
https://github.com/vivisect/vivisect
synced 2026-06-08 18:04:23 +00:00
88e3fafbde
* vivbin -S now starts the VivServer. -P sets the listening port. modifying VivServer comms to break up massive event dumps into max chunk sizes, and favor sending Memory Maps individually. this helps reduce timeouts while loading massive remote workspaces (working on a 500MB workspace right now). tweaking the timeouts and chunk sizes still. was taking 35 minutes to load, now down to ~16minutes over Wifi. still not happy, looking for other ways to enhance performance. i believe there are unnecessary delays in master, so i'm looking for them. * dramatic improvements on performance. now for a little cleanup. * some cleanup * cleaned up. should be gtg. * updates to cover unittests and other non-huge things * kill unused variable * better documentation for VivChunkQueue * more comment * more comment * solidly avoid race condition between Arch and Platform meta's being set before _snapInAnalysisModules() * metrics/monitoring on VivServer comms performance * changes per Rakuy0. configurable timeouts for Cobra and VivServer * Update vivisect/remote/server.py * timeo_* to timeout_* per Rakuy0 :) makes sense to me. * Apply suggestions from code review per rakuy0 * remove "block" options from getServerWorkspace. it's no longer part of the solution.
568 lines
20 KiB
Python
568 lines
20 KiB
Python
import os
|
|
import sys
|
|
import time
|
|
import cobra
|
|
import queue
|
|
import logging
|
|
import argparse
|
|
import threading
|
|
|
|
import cobra.dcode
|
|
|
|
import envi.config as e_config
|
|
import envi.common as e_common
|
|
import envi.threads as e_threads
|
|
|
|
import vivisect
|
|
import vivisect.cli as v_cli
|
|
import vivisect.parsers as v_parsers
|
|
import vivisect.storage.basicfile as viv_basicfile
|
|
|
|
from vivisect.const import *
|
|
from vivisect.defconfig import *
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
viv_port = 0x4074
|
|
|
|
viv_s_ip = '224.56.56.56'
|
|
viv_s_port = 26998
|
|
|
|
# DEFAULTS for non-configured use
|
|
timeout_wait = 10
|
|
timeout_sock = 50
|
|
timeout_aban = 120 # 2 minute timeout for abandon
|
|
|
|
# This should *only* rev when they're truly incompatible
|
|
server_version = 20130820
|
|
|
|
|
|
class VivServerClient:
|
|
'''
|
|
Implement "glue" methods for the vivisect workspace client to
|
|
talk to the server...
|
|
'''
|
|
def __init__(self, vw, server, wsname):
|
|
self.vw = vw
|
|
self.chan = None
|
|
self.wsname = wsname
|
|
self.server = server
|
|
self._processlock = threading.Lock()
|
|
self.eoffset = 0
|
|
self.q = queue.Queue() # The actual local Q we deliver to
|
|
self._qtime = time.time()
|
|
self._qcount = 0
|
|
self._qcountfull = 0
|
|
self._starttime = time.time()
|
|
|
|
@e_threads.firethread
|
|
def _eatServerEvents(self):
|
|
while True:
|
|
logger.debug("Starting a Workspace Event Processing run...")
|
|
count = 0
|
|
with self._processlock:
|
|
for event in self.server.getNextEvents(self.chan):
|
|
count += 1
|
|
self.q.put(event)
|
|
logger.debug("Workspace Event Processing run Complete. (%d events processed)", count)
|
|
time.sleep(.01)
|
|
|
|
def vprint(self, msg):
|
|
return self.server.vprint(msg)
|
|
|
|
def _fireEvent(self, event, einfo, local=False, skip=None):
|
|
return self.server._fireEvent(self.chan, event, einfo, local=local, skip=skip)
|
|
|
|
def createEventChannel(self):
|
|
self.chan = self.server.createEventChannel(self.wsname)
|
|
self._monitorEventQueuePerf()
|
|
logger.debug("VivServerClient: Event Channel created, firing _eatServerEvents() in separate thread")
|
|
self._eatServerEvents()
|
|
logger.debug("Waiting for initial Workspace Events to finish processing")
|
|
self.waitForCurEvents()
|
|
logger.debug("Done processing initial Workspace Events, now back to our program")
|
|
|
|
return self.chan
|
|
|
|
def waitForCurEvents(self):
|
|
'''
|
|
Since _eatServerEvents() runs in its own thread, we just wait for it's
|
|
_processlock to be released, signaling that it's done processing that
|
|
batch of VivEvents. We don't need to do anything with the lock, so
|
|
we just release the lock and return
|
|
'''
|
|
logger.debug("waiting for current Workspace Event run to complete...")
|
|
with self._processlock:
|
|
pass
|
|
|
|
def exportWorkspace(self):
|
|
# Retrieve the big initial list of viv events
|
|
return self.server.getNextEvents(self.chan)
|
|
|
|
@e_threads.firethread
|
|
def _monitorEventQueuePerf(self):
|
|
self._starttime = time.time()
|
|
while True:
|
|
qlen = -1
|
|
try:
|
|
time.sleep(1)
|
|
q = self.q
|
|
|
|
if q:
|
|
qlen = q.qsize()
|
|
if qlen % 100 == 1:
|
|
logger.debug("qlen: %d (%d/%d)", qlen, self._qcount, self._qcountfull)
|
|
|
|
curtime = time.time()
|
|
delta = curtime - self._qtime
|
|
if delta > 1 and self._qcount:
|
|
self._qtime = curtime
|
|
|
|
self._qrate = self._qcount / delta
|
|
logger.info("Event Queue Rate: %d/sec (%d / %d)", self._qrate, delta, (self._qtime-self._starttime))
|
|
logger.debug(" qlen: %d (%d/%d)", qlen, self._qcount, self._qcountfull)
|
|
|
|
# reset counter
|
|
self._qcount = 0
|
|
except:
|
|
logger.warning("Problem in Monitor Thread", exc_info=1)
|
|
|
|
|
|
def waitForEvent(self, chan, timeout=None):
|
|
q = self.q
|
|
qitem = q.get(timeout=timeout)
|
|
self._qcount += 1
|
|
self._qcountfull += 1
|
|
return qitem
|
|
|
|
def getLeaderLocations(self):
|
|
try:
|
|
return self.server.getLeaderLocations(self.wsname)
|
|
except Exception as e:
|
|
logger.warning("error in getLeaderLocations(): %r (is server up to date?)" % e)
|
|
return {}
|
|
|
|
def getLeaderSessions(self):
|
|
try:
|
|
return self.server.getLeaderSessions(self.wsname)
|
|
except Exception as e:
|
|
logger.warning("error in getLeaderSessions(): %r (is server up to date?)" % e)
|
|
return {}
|
|
|
|
|
|
class VivServer:
|
|
|
|
def __init__(self, dirname='', **kwargs):
|
|
self.path = os.path.abspath(dirname)
|
|
|
|
cfgdir = kwargs.get('confdir', None)
|
|
if cfgdir:
|
|
self.vivhome = os.path.abspath(cfgdir)
|
|
else:
|
|
self.vivhome = e_config.gethomedir(".viv", makedir=True)
|
|
cfgpath = os.path.join(self.vivhome, 'viv.json')
|
|
self.config = e_config.EnviConfig(filename=cfgpath, defaults=defconfig, docs=docconfig, autosave=True)
|
|
|
|
self.timeout_wait = self.config.viv.remote.timeout_wait
|
|
self.timeout_aban = self.config.viv.remote.timeout_aban
|
|
self.wsdict = {}
|
|
self.chandict = {}
|
|
self.wslock = threading.Lock()
|
|
|
|
self._loadWorkspaces()
|
|
self._maintThread()
|
|
self._saveWorkspaceThread()
|
|
|
|
def vprint(self, msg):
|
|
print(msg)
|
|
|
|
def getServerVersion(self):
|
|
return server_version
|
|
|
|
@e_threads.maintthread(1)
|
|
def _maintThread(self):
|
|
|
|
for chan in list(self.chandict.keys()):
|
|
chaninfo = self.chandict.get(chan)
|
|
# NOTE: double check because we're lock free...
|
|
if chaninfo is None:
|
|
continue
|
|
|
|
wsinfo, queue, chanleaders = chaninfo
|
|
if queue.abandoned(self.timeout_aban):
|
|
self.cleanupChannel(chan)
|
|
|
|
@e_threads.maintthread(30)
|
|
def _saveWorkspaceThread(self):
|
|
for wsinfo in self.wsdict.values():
|
|
lock, path, events, users, leaders, leaderloc = wsinfo
|
|
if events:
|
|
with lock:
|
|
wsinfo[2] = [] # start a new events list...
|
|
|
|
viv_basicfile.vivEventsAppendFile(path, events)
|
|
|
|
def _req_wsinfo(self, wsname):
|
|
wsinfo = self.wsdict.get(wsname)
|
|
if wsinfo is None:
|
|
raise Exception('Invalid Workspace Name: %s' % wsname)
|
|
return wsinfo
|
|
|
|
def listWorkspaces(self):
|
|
'''
|
|
Get a list of the workspaces this server is willing to share.
|
|
'''
|
|
return list(self.wsdict.keys())
|
|
|
|
def addNewWorkspace(self, wsname, events):
|
|
wspath = os.path.join(self.path, wsname)
|
|
wspath = os.path.realpath(wspath)
|
|
if not wspath.startswith(self.path):
|
|
raise Exception('Nice try liberty... ;)')
|
|
|
|
if os.path.exists(wspath):
|
|
raise Exception('Workspace %s already exists!' % wsname)
|
|
|
|
wsdir = os.path.dirname(wspath)
|
|
if not os.path.isdir(wsdir):
|
|
os.makedirs(wsdir, 0o750)
|
|
|
|
viv_basicfile.vivEventsToFile(wspath, events)
|
|
wsinfo = [threading.Lock(), wspath, [], {}, {}, {}]
|
|
self.wsdict[wsname] = wsinfo
|
|
|
|
def _loadWorkspaces(self):
|
|
|
|
# First, ditch any that are missing
|
|
for wsname in self.wsdict.keys():
|
|
wsinfo = self.wsdict.get(wsname)
|
|
if not os.path.isfile(wsinfo[1]):
|
|
self.wsdict.pop(wsname, None)
|
|
|
|
for dirname, dirnames, filenames in os.walk(self.path):
|
|
for filename in filenames:
|
|
wspath = os.path.join(dirname, filename)
|
|
wsname = os.path.relpath(wspath, self.path)
|
|
|
|
if not os.path.isfile(wspath):
|
|
continue
|
|
|
|
with open(wspath, 'rb') as f:
|
|
ext = f.read(6)
|
|
|
|
if not ext:
|
|
continue
|
|
|
|
if not v_parsers.guessFormat(ext) in ('viv', 'mpviv'):
|
|
continue
|
|
|
|
wsinfo = self.wsdict.get(wsname)
|
|
if wsinfo is None:
|
|
# Initialize the workspace info tuple
|
|
lock = threading.Lock()
|
|
wsinfo = [lock, wspath, [], {}, {}, {}]
|
|
logger.debug('loaded: %s', wsname)
|
|
self.wsdict[wsname] = wsinfo
|
|
|
|
def getNextEvents(self, chan):
|
|
chaninfo = self.chandict.get(chan)
|
|
if chaninfo is None:
|
|
raise Exception('Invalid Channel: %s' % chan)
|
|
logger.debug("getNextEvents(%r)", repr(chaninfo))
|
|
return chaninfo[1].get(timeout=self.timeout_wait)
|
|
|
|
# All APIs from here down are basically mirrors of the workspace APIs
|
|
# used with remote workspaces, with a prepended chan first argument
|
|
|
|
def _fireEvent(self, chan, event, einfo, local=False, skip=None):
|
|
#print("_fireEvent: %r %r %r %r %r" % (chan, event, einfo, local, skip))
|
|
|
|
if chan in self.chandict:
|
|
wsinfo, q, chanleaders = self.chandict.get(chan)
|
|
lock, fpath, pevents, users, leaders, leaderloc = wsinfo
|
|
oldclient = False
|
|
|
|
elif chan in self.wsdict:
|
|
# DEPRECATED: this is for backwards compat. use only the chandict code one year from today, 5/10/2022.
|
|
wsname = chan
|
|
wsinfo = self._req_wsinfo(wsname)
|
|
lock, fpath, pevents, users, _, _ = wsinfo
|
|
# cheat for older clients... they don't get all the follow-the-leader goodness until they upgrade
|
|
chanleaders = []
|
|
leaders = {}
|
|
leaderloc = {}
|
|
oldclient = True
|
|
|
|
else:
|
|
raise Exception("BAD CHANNEL: _fireEvent: %r %r %r %r %r" % (chan, event, einfo, local, skip))
|
|
|
|
evtup = (event, einfo)
|
|
with lock:
|
|
# Transient events do not get saved
|
|
if not event & VTE_MASK:
|
|
pevents.append(evtup)
|
|
|
|
else:
|
|
vtevent = event ^ VTE_MASK
|
|
if vtevent == VTE_FOLLOWME:
|
|
pass
|
|
|
|
elif vtevent == VTE_IAMLEADER:
|
|
logger.info("VTE_IAMLEADER: %r" % repr(evtup))
|
|
uuid, user, fname, locexpr = einfo
|
|
leaders[uuid] = (user, fname)
|
|
leaderloc[uuid] = locexpr
|
|
chanleaders.append(uuid)
|
|
|
|
elif vtevent == VTE_FOLLOWME:
|
|
logger.info("VTE_FOLLOWME: %r" % repr(evtup))
|
|
uuid, expr = einfo
|
|
leaderloc[uuid] = expr
|
|
|
|
elif vtevent == VTE_KILLLEADER:
|
|
logger.info("VTE_KILLLEADER: %r" % repr(evtup))
|
|
uuid = einfo
|
|
leaders.pop(uuid, None)
|
|
leaderloc.pop(uuid, None)
|
|
|
|
elif vtevent == VTE_MODLEADER:
|
|
logger.info("VTE_MODLEADER: %r" % repr(evtup))
|
|
uuid, user, fname = einfo
|
|
leaders[uuid] = (user, fname)
|
|
|
|
|
|
# SPEED HACK
|
|
[q.append(evtup) for chan, q in users.items() if chan != skip]
|
|
|
|
def createEventChannel(self, wsname):
|
|
wsinfo = self._req_wsinfo(wsname)
|
|
chan = e_common.hexify(os.urandom(16))
|
|
chunksize = self.config.viv.server.queue_chunksize
|
|
|
|
# Load up the queue with events from the file, and current events
|
|
lock, fpath, pevents, users, leaders, leaderloc = wsinfo
|
|
|
|
with lock:
|
|
# These must reference the same actual list object...
|
|
queue = VivChunkQueue(chunksize=chunksize)
|
|
users[chan] = queue
|
|
chanleaders = []
|
|
self.chandict[chan] = [wsinfo, queue, chanleaders]
|
|
|
|
# add events after the channel is created. that way the client can start pulling event groups immediately
|
|
fileevts = viv_basicfile.vivEventsFromFile(fpath)
|
|
fileevts.extend(pevents)
|
|
# use this firethread function to add events while we return the channel
|
|
queue.chunkedAddLargeEventList(fileevts)
|
|
|
|
return chan
|
|
|
|
def getLeaderLocations(self, wsname):
|
|
wsinfo = self._req_wsinfo(wsname)
|
|
lock, path, events, users, leaders, leaderloc = wsinfo
|
|
return dict(leaderloc)
|
|
|
|
def getLeaderSessions(self, wsname):
|
|
wsinfo = self._req_wsinfo(wsname)
|
|
lock, path, events, users, leaders, leaderloc = wsinfo
|
|
return dict(leaders)
|
|
|
|
def cleanupChannel(self, chan):
|
|
chantup = self.chandict.get(chan, None)
|
|
if chantup is None:
|
|
logger.warning("Attempting to clean up nonexistent channel: %r" % chan)
|
|
return
|
|
|
|
# Remove all channels originating from this channel
|
|
wsinfo, queue, chanleaders = chantup
|
|
for uuid in chanleaders:
|
|
self._fireEvent(chan, VTE_KILLLEADER | VTE_MASK, uuid)
|
|
|
|
# Remove from the workspace clients
|
|
lock, fpath, pevents, users, leaders, leaderloc = wsinfo
|
|
with lock:
|
|
userinfo = users.pop(chan, None)
|
|
|
|
# Remove from our chandict
|
|
self.chandict.pop(chan, None)
|
|
|
|
# TODO: queue events to a list... after a few ms (or CHUNKSIZE reached), convert to a Dict/List and send it?
|
|
|
|
class VivChunkQueue(e_threads.ChunkQueue):
|
|
'''
|
|
Vivisect Event specific version of the ChunkQueue
|
|
This ChunkQueue implements ChunkQueue helpers specifically designed to
|
|
handle VivServer requirements, namely:
|
|
1) Chunking Around VWE_ADDMMAP events to reduce delays between request to
|
|
response
|
|
2) Front-loading chunking of the initial file VivServer download. Again,
|
|
this helps avoid timeouts while processing large workspaces. With
|
|
the addition of multi-library VivWorkspaces, this can be a *real*
|
|
problem. VivChunkQueue helps solve those problems
|
|
3) Introduction of the Event Group, enabling the functionality required
|
|
to do #2 efficiently. Front-loading pre-analyzed groups of events
|
|
speeds up loading dramatically
|
|
'''
|
|
def _get_items(self):
|
|
'''
|
|
Already has self.lock when called.
|
|
'''
|
|
if not len(self.items):
|
|
return []
|
|
|
|
# watch for event groups
|
|
# "event groups" are a dict with a list stored at key 0. this list is
|
|
# then able to be shoved over the Cobra channel immediately, front-loading
|
|
# any preparation, and drastically speeding up initial message glut on
|
|
# workspace loading.
|
|
if type(self.items[0]) == dict:
|
|
evtgroup = self.items.pop(0)
|
|
evts = evtgroup[0]
|
|
logger.debug("_get_items: STORED EVENT GROUP of size %d (queue remaining: %d)", len(evts), len(self.items))
|
|
return evts
|
|
|
|
# handle events in "chunks"
|
|
if self.chunksize:
|
|
logger.debug('_get_items(): chunksize: %d', self.chunksize)
|
|
|
|
# search for ADDMMAPs so we don't group two in one transmission
|
|
# RAM and CPU utilization make larger workspaces time out otherwise
|
|
# especially if there are multiple large memory maps (eg. multi-lib
|
|
# workspaces)
|
|
idx = None
|
|
for idx in range(self.chunksize-1):
|
|
# break if we reach the end of the event list
|
|
if idx >= len(self.items):
|
|
break
|
|
|
|
evtitem = self.items[idx]
|
|
|
|
# search for Event Groups
|
|
if type(evtitem) == dict:
|
|
# don't include this in the list! kick out and let the
|
|
# "event group" section (above) handle this one
|
|
idx -= 1
|
|
break
|
|
|
|
evtype, evtdata = evtitem
|
|
# search for ADDMMAP events
|
|
if evtype == vivisect.VWE_ADDMMAP:
|
|
break
|
|
|
|
|
|
# idx+1 to grabs the last item identified (except for Event Groups)
|
|
ret = self.items[:idx+1]
|
|
|
|
numEaten = len(ret)
|
|
# just rebuild the list - tried several options, this still won out
|
|
self.items = self.items[numEaten:]
|
|
|
|
numLeft = len(self.items)
|
|
logger.debug('_get_items() - returning (%d eaten / %d left)', numEaten, numLeft)
|
|
|
|
else:
|
|
# if chunksize is 0 or None, go old-school and just shove it all in
|
|
ret = self.items
|
|
for idx, (evtype, evtdata) in enumerate(ret):
|
|
# still want to only send one memory map at a time
|
|
if evtype == vivisect.VWE_ADDMMAP:
|
|
ret = self.items[:idx+1]
|
|
break
|
|
|
|
self.items = self.items[len(ret):]
|
|
logger.debug("_get_items() - returning %d items (%d remaining)", len(ret), len(self.items))
|
|
return ret
|
|
|
|
@e_threads.firethread
|
|
def chunkedAddLargeEventList(self, evts):
|
|
'''
|
|
Break up a large list of events into appropriately pre-sized chunks,
|
|
allowing for high efficiency in serving them. This is a @firethread
|
|
function, meaning that it runs in its own thread, making it "fire-and-
|
|
forget".
|
|
'''
|
|
evtcount = len(evts)
|
|
idx = 0
|
|
logger.debug("... adding %d events to the Queue", evtcount)
|
|
while evtcount > 0:
|
|
anchor = idx
|
|
for count in range(self.chunksize):
|
|
# pick and poke...
|
|
evt = evts[idx]
|
|
idx += 1
|
|
evtcount -= 1
|
|
if evtcount <10:
|
|
logger.debug("...evtcount==%d, idx==%d", evtcount, idx)
|
|
if evtcount == 0:
|
|
# we don't want to clip the last entry
|
|
idx += 1
|
|
break
|
|
|
|
|
|
# if it's a memory map, only include one per chunk
|
|
if evt[0] == vivisect.VWE_ADDMMAP:
|
|
logger.debug(" (chopping after VWE_ADDMMAP)")
|
|
break
|
|
|
|
# here is the list of events we're packaging up
|
|
worklist = evts[anchor:idx]
|
|
|
|
# we package the list in a dict, to differentiate between normal events and event groups
|
|
workdict = {0 : worklist}
|
|
|
|
# add this group to the Queue
|
|
logger.debug("... appending %d event chunk to the Queue (%d:%d)", len(worklist), anchor, idx)
|
|
self.append(workdict)
|
|
|
|
|
|
|
|
def getServerWorkspace(server, wsname):
|
|
vw = v_cli.VivCli()
|
|
logger.debug("Starting: VivServerClient(%r, %r)", server, wsname)
|
|
cliproxy = VivServerClient(vw, server, wsname)
|
|
logger.debug("Starting: vw.initWorkspaceClient()")
|
|
vw.initWorkspaceClient(cliproxy)
|
|
logger.debug("Complete: vw.initWorkspaceClient()")
|
|
return vw
|
|
|
|
|
|
def connectToServer(hostname, port=viv_port):
|
|
builder = cobra.initSocketBuilder(hostname, port)
|
|
builder.setTimeout(timeout_sock)
|
|
server = cobra.CobraProxy("cobra://%s:%d/VivServer" % (hostname, port), msgpack=True)
|
|
version = server.getServerVersion()
|
|
if version != server_version:
|
|
raise Exception('Incompatible Server: his ver: %d our ver: %d' % (version, server_version))
|
|
return server
|
|
|
|
|
|
def runMainServer(dirname='', port=viv_port):
|
|
s = VivServer(dirname=dirname)
|
|
daemon = cobra.CobraDaemon(port=port, msgpack=True)
|
|
daemon.recvtimeout = timeout_sock
|
|
daemon.shareObject(s, 'VivServer')
|
|
daemon.serve_forever()
|
|
|
|
|
|
def setup():
|
|
ap = argparse.ArgumentParser('Start a vivisect server to share workspaces')
|
|
ap.add_argument('dirn', help='A directory full of *.viv files to share')
|
|
ap.add_argument('--port', '-p', type=int, default=viv_port,
|
|
help='The port to start server on (defaults to %d)' % viv_port)
|
|
return ap
|
|
|
|
|
|
def main():
|
|
opts = setup().parse_args()
|
|
vdir = os.path.abspath(opts.dirn)
|
|
if not os.path.isdir(vdir):
|
|
logger.error('%s is not a valid directory!', vdir)
|
|
return -1
|
|
|
|
print(f'VivServer starting (port: {opts.port})')
|
|
runMainServer(vdir, opts.port)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|