# Written by Arno Bakker, Diego Rabioli # see LICENSE.txt for license information # # TODO: # - Switch to SIMPLE+METADATA query # # - adjust SIMPLE+METADATA such that it returns P2PURLs if possible. # - DO NOT SAVE P2PURLs as .torrent, put in 'torrent_file_name' field in DB. # # - Implement continuous dump of results to JS. I.e. push sorting and rendering to browser. # * One option is RFC5023: Atom Pub Proto, $10.1 "Collecting Partial Lists" I.e. # return a partial list and add a >sys.stderr,"searchmap: Parsed",o qdict = cgi.parse_qs(o.query) print >>sys.stderr,"searchmap: qdict",qdict searchstr = qdict['q'][0] searchstr = searchstr.strip() collection = qdict['collection'][0] metafeedurl = qdict['metafeed'][0] print >>sys.stderr,"searchmap: searchstr",`searchstr` # Garbage collect: self.id2hits.garbage_collect_timestamp_smaller(time.time() - HITS_TIMEOUT) if collection == "metafeed": if not self.check_reload_metafeed(metafeedurl): return {'statuscode':504, 'statusmsg':'504 MetaFeed server did not respond'} return self.process_search_metafeed(searchstr) else: return self.process_search_p2p(searchstr) def process_search_metafeed(self,searchstr): """ Search for hits in the ATOM feeds we got from the meta feed """ allhits = [] for feedurl in self.metafp.get_feedurls(): feedp = FeedParser(feedurl) try: feedp.parse() except: # TODO: return 504 gateway error if none of the feeds return anything print_exc() hits = feedp.search(searchstr) allhits.extend(hits) for hitentry in allhits: titleelement = hitentry.find('{http://www.w3.org/2005/Atom}title') print >>sys.stderr,"bg: search: meta: Got hit",titleelement.text id = str(random.random())[2:] atomurlpathprefix = URLPATH_HITS_PREFIX+'/'+str(id) atomxml = feedhits2atomxml(allhits,searchstr,atomurlpathprefix) atomstream = StringIO(atomxml) atomstreaminfo = { 'statuscode':200,'mimetype': 'application/atom+xml', 'stream': atomstream, 'length': len(atomxml)} return atomstreaminfo def process_search_p2p(self,searchstr): """ Search for hits in local database and perform remote query. EXPERIMENTAL: needs peers with SIMPLE+METADATA query support. """ # Initially, searchstr = keywords keywords = searchstr.split() id = str(random.random())[2:] self.id2hits.add_query(id,searchstr,time.time()) # Parallel: initiate remote query q = 'SIMPLE '+searchstr print >>sys.stderr,"bg: search: p2p: Remote query for",q got_remote_hits_lambda = lambda permid,query,remotehits:self.sesscb_got_remote_hits(id,permid,query,remotehits) self.st = time.time() self.session.query_connected_peers(q,got_remote_hits_lambda,max_peers_to_query=20) # Query local DB while waiting torrent_db = self.session.open_dbhandler(NTFY_TORRENTS) localdbhits = torrent_db.searchNames(keywords) print >>sys.stderr,"bg: Local hits",len(localdbhits) self.session.close_dbhandler(torrent_db) # Convert list to dict keyed by infohash localhits = localdbhits2hits(localdbhits) self.id2hits.add_hits(id,localhits) # TODO ISSUE: incremental display of results to user? How to implement this? atomurlpathprefix = URLPATH_HITS_PREFIX+'/'+str(id) nextlinkpath = atomurlpathprefix atomhits = hits2atomhits(localhits,atomurlpathprefix) atomxml = atomhits2atomxml(atomhits,searchstr,atomurlpathprefix,nextlinkpath=nextlinkpath) atomstream = StringIO(atomxml) atomstreaminfo = { 'statuscode':200,'mimetype': 'application/atom+xml', 'stream': atomstream, 'length': len(atomxml)} return atomstreaminfo def sesscb_got_remote_hits(self,id,permid,query,remotehits): # Called by SessionCallback thread try: et = time.time() diff = et - self.st print >>sys.stderr,"bg: sesscb_got_remote_hits",len(remotehits),"after",diff hits = remotehits2hits(remotehits) print >>sys.stderr,"bg: sesscb_got_remote_hits: common",len(hits) # TEST """ newhits = {} for infohash in hits.keys()[0:1]: hit = hits[infohash] newhits[infohash] = hit """ newhits = hits self.id2hits.add_hits(id,newhits) atomurlpathprefix = URLPATH_HITS_PREFIX+'/'+str(id) # Inform Plugin of feed. #self.ic.start_searchresults(atomurlpathprefix) except: print_exc() def check_reload_metafeed(self,metafeedurl): if self.metafeedurl is None or self.metafeedurl != metafeedurl: self.metafp = MetaFeedParser(metafeedurl) try: self.metafp.parse() # TODO: offload to separate thread? print >>sys.stderr,"bg: search: meta: Found feeds",self.metafp.get_feedurls() self.metafeedurl = metafeedurl except: print_exc() return False return True def localdbhits2hits(localdbhits): hits = {} for dbhit in localdbhits: localhit = {} localhit['hittype'] = "localdb" localhit.update(dbhit) infohash = dbhit['infohash'] # convenient to also have in record hits[infohash] = localhit return hits def remotehits2hits(remotehits): hits = {} for infohash,hit in remotehits.iteritems(): #print >>sys.stderr,"remotehit2hits: keys",hit.keys() remotehit = {} remotehit['hittype'] = "remote" #remotehit['query_permid'] = permid # Bit of duplication, ignore remotehit['infohash'] = infohash # convenient to also have in record remotehit.update(hit) # HACK: Create fake torrent file if not 'metadata' in hit: metatype = TSTREAM_MIME_TYPE metadata = hack_make_default_merkletorrent(hit['content_name']) remotehit['metatype'] = metatype remotehit['metadata'] = metadata hits[infohash] = remotehit return hits class Query2HitsMap: """ Stores localdb and remotehits in common hits format, i.e., each hit has a 'hittype' attribute that tells which type it is (localdb or remote). This Query2HitsMap is passed to the Hits2AnyPathMapper, which is connected to the internal HTTP server. The HTTP server will then forward all "/hits" GET requests to this mapper. The mapper then dynamically generates the required contents from the stored hits, e.g. an ATOM feed, MPEG7 description, .torrent file and thumbnail images from the torrent. """ def __init__(self): self.lock = RLock() self.d = {} def add_query(self,id,searchstr,timestamp): if DEBUG: print >>sys.stderr,"q2h: lock1",id self.lock.acquire() try: qrec = self.d.get(id,{}) qrec['searchstr'] = searchstr qrec['timestamp'] = timestamp qrec['hitlist'] = {} self.d[id] = qrec finally: if DEBUG: print >>sys.stderr,"q2h: unlock1" self.lock.release() def add_hits(self,id,hits): if DEBUG: print >>sys.stderr,"q2h: lock2",id,len(hits) self.lock.acquire() try: qrec = self.d[id] qrec['hitlist'].update(hits) finally: if DEBUG: print >>sys.stderr,"q2h: unlock2" self.lock.release() def get_hits(self,id): if DEBUG: print >>sys.stderr,"q2h: lock3",id self.lock.acquire() try: qrec = self.d[id] return copy.copy(qrec['hitlist']) # return shallow copy finally: if DEBUG: print >>sys.stderr,"q2h: unlock3" self.lock.release() def get_searchstr(self,id): if DEBUG: print >>sys.stderr,"q2h: lock4" self.lock.acquire() try: qrec = self.d[id] return qrec['searchstr'] finally: if DEBUG: print >>sys.stderr,"q2h: unlock4" self.lock.release() def garbage_collect_timestamp_smaller(self,timethres): self.lock.acquire() try: idlist = [] for id,qrec in self.d.iteritems(): if qrec['timestamp'] < timethres: idlist.append(id) for id in idlist: del self.d[id] finally: self.lock.release() class Hits2AnyPathMapper(AbstractPathMapper): """ See Query2Hits description """ def __init__(self,session,id2hits): self.session = session self.id2hits = id2hits def get(self,urlpath): """ Possible paths: /hits/id -> ATOM feed /hits/id/infohash.xml -> MPEG 7 /hits/id/infohash.tstream -> Torrent file /hits/id/infohash.tstream/thumbnail -> Thumbnail """ if DEBUG: print >>sys.stderr,"hitsmap: Got",urlpath if not urlpath.startswith(URLPATH_HITS_PREFIX): return streaminfo404() paths = urlpath.split('/') if len(paths) < 3: return streaminfo404() id = paths[2] if len(paths) == 3: # ATOM feed searchstr = self.id2hits.get_searchstr(id) hits = self.id2hits.get_hits(id) if DEBUG: print >>sys.stderr,"hitsmap: Found",len(hits),"hits" atomhits = hits2atomhits(hits,urlpath) if DEBUG: print >>sys.stderr,"hitsmap: Found",len(atomhits),"atomhits" atomxml = atomhits2atomxml(atomhits,searchstr,urlpath) #if DEBUG: # print >>sys.stderr,"hitsmap: atomstring is",`atomxml` atomstream = StringIO(atomxml) atomstreaminfo = { 'statuscode':200,'mimetype': 'application/atom+xml', 'stream': atomstream, 'length': len(atomxml)} return atomstreaminfo elif len(paths) >= 4: # Either NS Metadata, Torrent file, or thumbnail urlinfohash = paths[3] print >>sys.stderr,"hitsmap: path3 is",urlinfohash if urlinfohash.endswith(URLPATH_TORRENT_POSTFIX): # Torrent file, or thumbnail coded = urlinfohash[:-len(URLPATH_TORRENT_POSTFIX)] infohash = urlpath2infohash(coded) else: # NS Metadata / MPEG7 coded = urlinfohash[:-len(URLPATH_NSMETA_POSTFIX)] infohash = urlpath2infohash(coded) # Check if hit: hits = self.id2hits.get_hits(id) print >>sys.stderr,"hitsmap: meta: Found",len(hits),"hits" hit = hits.get(infohash,None) if hit is not None: if len(paths) == 5: # Thumbnail return self.get_thumbstreaminfo(infohash,hit) elif urlinfohash.endswith(URLPATH_TORRENT_POSTFIX): # Torrent file return self.get_torrentstreaminfo(infohash,hit) else: # NS Metadata / MPEG7 hiturlpathprefix = URLPATH_HITS_PREFIX+'/'+id return self.get_nsmetastreaminfo(infohash,hit,hiturlpathprefix,urlpath) return streaminfo404() def get_torrentstreaminfo(self,infohash,hit): if DEBUG: print >>sys.stderr,"hitmap: get_torrentstreaminfo",infohash2urlpath(infohash) torrent_db = self.session.open_dbhandler(NTFY_TORRENTS) try: if hit['hittype'] == "localdb": dbhit = torrent_db.getTorrent(infohash,include_mypref=False) colltorrdir = self.session.get_torrent_collecting_dir() filepath = os.path.join(colltorrdir,dbhit['torrent_file_name']) # Return stream that contains torrent file stream = open(filepath,"rb") length = os.path.getsize(filepath) torrentstreaminfo = {'statuscode':200,'mimetype':TSTREAM_MIME_TYPE,'stream':stream,'length':length} return torrentstreaminfo else: if hit['metatype'] == URL_MIME_TYPE: # Shouldn't happen, P2PURL should be embedded in atom return streaminfo404() else: stream = StringIO(hit['metadata']) length = len(hit['metadata']) torrentstreaminfo = {'statuscode':200,'mimetype':TSTREAM_MIME_TYPE,'stream':stream,'length':length} return torrentstreaminfo finally: self.session.close_dbhandler(torrent_db) def get_thumbstreaminfo(self,infohash,hit): if DEBUG: print >>sys.stderr,"hitmap: get_thumbstreaminfo",infohash2urlpath(infohash) torrent_db = self.session.open_dbhandler(NTFY_TORRENTS) try: if hit['hittype'] == "localdb": dbhit = torrent_db.getTorrent(infohash,include_mypref=False) colltorrdir = self.session.get_torrent_collecting_dir() filepath = os.path.join(colltorrdir,dbhit['torrent_file_name']) tdef = TorrentDef.load(filepath) (thumbtype,thumbdata) = tdef.get_thumbnail() return self.create_thumbstreaminfo(thumbtype,thumbdata) else: if hit['metatype'] == URL_MIME_TYPE: # Shouldn't happen, not thumb in P2PURL return streaminfo404() else: if DEBUG: print >>sys.stderr,"hitmap: get_thumbstreaminfo: looking for thumb in remote hit" metainfo = bdecode(hit['metadata']) tdef = TorrentDef.load_from_dict(metainfo) (thumbtype,thumbdata) = tdef.get_thumbnail() return self.create_thumbstreaminfo(thumbtype,thumbdata) finally: self.session.close_dbhandler(torrent_db) def create_thumbstreaminfo(self,thumbtype,thumbdata): if thumbtype is None: return streaminfo404() else: # Return stream that contains thumb stream = StringIO(thumbdata) length = len(thumbdata) thumbstreaminfo = {'statuscode':200,'mimetype':thumbtype,'stream':stream,'length':length} return thumbstreaminfo def get_nsmetastreaminfo(self,infohash,hit,hiturlpathprefix,hitpath): colltorrdir = self.session.get_torrent_collecting_dir() nsmetahit = hit2nsmetahit(hit,hiturlpathprefix,colltorrdir) if DEBUG: print >>sys.stderr,"hitmap: get_nsmetastreaminfo: nsmetahit is",`nsmetahit` nsmetarepr = nsmetahit2nsmetarepr(nsmetahit,hitpath) nsmetastream = StringIO(nsmetarepr) nsmetastreaminfo = { 'statuscode':200,'mimetype': 'text/xml', 'stream': nsmetastream, 'length': len(nsmetarepr)} return nsmetastreaminfo def infohash2urlpath(infohash): if len(infohash) != 20: raise ValueError("infohash len 20 !=" + str(len(infohash))) hex = binascii.hexlify(infohash) if len(hex) != 40: raise ValueError("hex len 40 !=" + str(len(hex))) return hex def urlpath2infohash(hex): if len(hex) != 40: raise ValueError("hex len 40 !=" + str(len(hex)) + " " + hex) infohash = binascii.unhexlify(hex) if len(infohash) != 20: raise ValueError("infohash len 20 !=" + str(len(infohash))) return infohash def hits2atomhits(hits,urlpathprefix): atomhits = {} for infohash,hit in hits.iteritems(): if hit['hittype'] == "localdb": atomhit = localdbhit2atomhit(hit,urlpathprefix) atomhits[infohash] = atomhit else: atomhit = remotehit2atomhit(hit,urlpathprefix) atomhits[infohash] = atomhit return atomhits def localdbhit2atomhit(dbhit,urlpathprefix): atomhit = {} atomhit['title'] = htmlfilter(unicode2iri(dbhit['name'])) atomhit['summary'] = htmlfilter(unicode2iri(dbhit['comment'])) if dbhit['thumbnail']: urlpath = urlpathprefix+'/'+infohash2urlpath(dbhit['infohash'])+URLPATH_TORRENT_POSTFIX+URLPATH_THUMBNAIL_POSTFIX atomhit['p2pnext:image'] = urlpath return atomhit def remotehit2atomhit(remotehit,urlpathprefix): # TODO: make RemoteQuery return full DB schema of TorrentDB #print >>sys.stderr,"remotehit2atomhit: keys",remotehit.keys() atomhit = {} atomhit['title'] = htmlfilter(remotehit['content_name']) atomhit['summary'] = "Seeders: "+str(remotehit['seeder'])+" Leechers: "+str(remotehit['leecher']) if remotehit['metatype'] != URL_MIME_TYPE: # TODO: thumbnail, see if we can detect presence (see DB schema remark). # Now we assume it's always there if not P2PURL urlpath = urlpathprefix+'/'+infohash2urlpath(remotehit['infohash'])+URLPATH_TORRENT_POSTFIX+URLPATH_THUMBNAIL_POSTFIX atomhit['p2pnext:image'] = urlpath return atomhit def htmlfilter(s): """ Escape characters to which HTML parser is sensitive """ if s is None: return "" news = s news = news.replace('&','&') news = news.replace('<','<') news = news.replace('>','>') return news def atomhits2atomxml(atomhits,searchstr,urlpathprefix,nextlinkpath=None): # TODO: use ElementTree parser here too, see AtomFeedParser:feedhits2atomxml atom = '' atom += '\n' atom += '\n' atom += ' Hits for '+searchstr+'\n' atom += ' \n' if nextlinkpath: atom += ' \n' atom += ' \n' atom += ' NSSA\n' atom += ' \n' atom += ' urn:nssa\n' atom += ' '+now2formatRFC3339()+'\n' #atom += '\n' # TODO for infohash,hit in atomhits.iteritems(): urlinfohash = infohash2urlpath(infohash) hitpath = urlpathprefix+'/'+urlinfohash+URLPATH_NSMETA_POSTFIX atom += ' \n' atom += ' '+hit['title']+'\n' atom += ' \n' atom += ' urn:nssa-'+urlinfohash+'\n' atom += ' '+now2formatRFC3339()+'\n' if hit['summary'] is not None: atom += ' '+hit['summary']+'\n' if 'p2pnext:image' in hit: atom += ' \n' atom += ' \n' atom += '\n' return atom def now2formatRFC3339(): formatstr = "%Y-%m-%dT%H:%M:%S" s = time.strftime(formatstr, time.gmtime()) s += 'Z' return s def hit2nsmetahit(hit,hiturlprefix,colltorrdir): """ Convert common hit to the fields required for the MPEG7 NS metadata """ print >>sys.stderr,"his2nsmetahit:" # Read info from torrent files / P2PURLs if hit['hittype'] == "localdb": name = hit['name'] if hit['torrent_file_name'].startswith(P2PURL_SCHEME): # Local DB hit that is P2PURL torrenturl = hit['torrent_file_name'] titleimgurl = None tdef = TorrentDef.load_from_url(torrenturl) else: # Local DB hit that is torrent file torrenturlpath = '/'+infohash2urlpath(hit['infohash'])+URLPATH_TORRENT_POSTFIX torrenturl = hiturlprefix + torrenturlpath filepath = os.path.join(colltorrdir,hit['torrent_file_name']) tdef = TorrentDef.load(filepath) (thumbtype,thumbdata) = tdef.get_thumbnail() if thumbtype is None: titleimgurl = None else: titleimgurl = torrenturl+URLPATH_THUMBNAIL_POSTFIX else: # Remote hit name = hit['content_name'] if hit['metatype'] == URL_MIME_TYPE: torrenturl = hit['torrent_file_name'] titleimgurl = None tdef = TorrentDef.load_from_url(torrenturl) else: torrenturlpath = '/'+infohash2urlpath(hit['infohash'])+URLPATH_TORRENT_POSTFIX torrenturl = hiturlprefix + torrenturlpath metainfo = bdecode(hit['metadata']) tdef = TorrentDef.load_from_dict(metainfo) (thumbtype,thumbdata) = tdef.get_thumbnail() if thumbtype is None: titleimgurl = None else: titleimgurl = torrenturl+URLPATH_THUMBNAIL_POSTFIX # Extract info required for NS metadata MPEG7 representation. nsmetahit = {} nsmetahit['title'] = unicode2iri(name) nsmetahit['titleimgurl'] = titleimgurl comment = tdef.get_comment() if comment is None: nsmetahit['abstract'] = None else: nsmetahit['abstract'] = unicode2iri(comment) nsmetahit['producer'] = 'Insert Name Here' creator = tdef.get_created_by() if creator is None: creator = 'Insert Name Here Too' nsmetahit['disseminator'] = creator nsmetahit['copyrightstr'] = 'Copyright '+creator nsmetahit['torrent_url'] = torrenturl # TODO: multifile torrents, LIVE nsmetahit['duration'] = bitratelength2nsmeta_duration(tdef.get_bitrate(),tdef.get_length()) return nsmetahit def unicode2iri(uni): # Roughly after http://www.ietf.org/rfc/rfc3987.txt Sec 3.1 procedure. # TODO: do precisely after. s = uni.encode('UTF-8') return urllib.quote(s) def bitratelength2nsmeta_duration(bitrate,length): # Format example: PT0H15M0S if bitrate is None: return 'PT01H00M0S' # 1 hour secs = float(length)/float(bitrate) hours = float(int(secs / 3600.0)) secs = secs - hours*3600.0 mins = float(int(secs / 60.0)) secs = secs - mins*60.0 return 'PT%02.0fH%02.0fM%02.0fS' % (hours,mins,secs) def nsmetahit2nsmetarepr(hit,hitpath): title = hit['title'] titleimgurl = hit['titleimgurl'] abstract = hit['abstract'] producer = hit['producer'] disseminator = hit['disseminator'] copyrightstr = hit['copyrightstr'] torrenturl = hit['torrent_url'] duration = hit['duration'] # Format example: PT0H15M0S livetimepoint = now2formatRFC3339() # Format example: '2009-10-05T00:40:00+01:00' # TODO VOD s = '' s += '\n' s += '\n' s += ' \n' s += ' \n' s += ' \n' s += ' '+title+'\n' s += ' \n' if titleimgurl: s += ' \n' s += ' '+titleimgurl+'\n' s += ' \n' s += ' \n' if abstract: s += ' \n' s += ' '+abstract+'\n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' '+producer+'\n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' '+disseminator+'\n' s += ' \n' s += ' \n' s += ' '+copyrightstr+'\n' s += ' \n' s += ' \n' s += ' false\n' s += ' false\n' s += ' false\n' s += ' \n' s += ' \n' s += ' '+torrenturl+'\n' s += ' \n' s += ' offset(0, 1000)\n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += ' '+livetimepoint+'\n' s += ' \n' s += ' \n' s += ' \n' s += ' \n' s += '\n' return s def hack_make_default_merkletorrent(title): metainfo = {} metainfo['announce'] = 'http://localhost:0/announce' metainfo['creation date'] = int(time.time()) info = {} info['name'] = title info['length'] = 2 ** 30 info['piece length'] = 2 ** 16 info['root hash'] = '*' * 20 metainfo['info'] = info mdict = {} mdict['Publisher'] = 'Tribler' mdict['Description'] = '' mdict['Progressive'] = 1 mdict['Speed Bps'] = str(2 ** 16) mdict['Title'] = metainfo['info']['name'] mdict['Creation Date'] = long(time.time()) # Azureus client source code doesn't tell what this is, so just put in random value from real torrent mdict['Content Hash'] = 'PT3GQCPW4NPT6WRKKT25IQD4MU5HM4UY' mdict['Revision Date'] = long(time.time()) cdict = {} cdict['Content'] = mdict metainfo['azureus_properties'] = cdict return bencode(metainfo) """ class Infohash2TorrentPathMapper(AbstractPathMapper): Mapper to map in the collection of known torrents files (=collected + started + own) into the HTTP address space of the local HTTP server. In particular, it maps a "/infohash/aabbccdd...zz.tstream" path to a streaminfo dict. Also supported are "/infohash/aabbccdd...zz.tstream/thumbnail" queries, which try to read the thumbnail from the torrent. def __init__(self,urlpathprefix,session): self.urlpathprefix = urlpathprefix self.session = session self.torrent_db = self.session.open_dbhandler(NTFY_TORRENTS) def get(self,urlpath): if not urlpath.startswith(self.urlpathprefix): return None try: wantthumb = False if urlpath.endswith(URLPATH_THUMBNAIL_POSTFIX): wantthumb = True infohashquote = urlpath[len(self.urlpathprefix):-len(URLPATH_TORRENT_POSTFIX+URLPATH_THUMBNAIL_POSTFIX)] else: infohashquote = urlpath[len(self.urlpathprefix):-len(URLPATH_TORRENT_POSTFIX)] infohash = urlpath2infohash(infohash) dbhit = self.torrent_db.getTorrent(infohash,include_mypref=False) colltorrdir = self.session.get_torrent_collecting_dir() filepath = os.path.join(colltorrdir,dbhit['torrent_file_name']) if not wantthumb: # Return stream that contains torrent file stream = open(filepath,"rb") length = os.path.getsize(filepath) streaminfo = {'statuscode':200,'mimetype':TSTREAM_MIME_TYPE,'stream':stream,'length':length} else: # Return stream that contains thumbnail tdef = TorrentDef.load(filepath) (thumbtype,thumbdata) = tdef.get_thumbnail() if thumbtype is None: return None else: stream = StringIO(thumbdata) streaminfo = {'statuscode':200,'mimetype':thumbtype,'stream':stream,'length':len(thumbdata)} return streaminfo except: print_exc() return None """