Version 119

This commit is contained in:
Hydrus 2014-06-18 16:53:48 -05:00
parent 6ecbca90fd
commit babac9e405
17 changed files with 538 additions and 44 deletions

3
bin/ffmpeg_readme.txt Normal file
View File

@ -0,0 +1,3 @@
If you are running from source and want videos to render, stick ffmpeg.exe (for windows) or ffmpeg executable file (for linux/osx) in this folder.
You can get a static build at http://ffmpeg.org/download.html

View File

@ -17,6 +17,7 @@ from include import HydrusConstants as HC
import os
import sys
import time
from include import ClientController
import threading
@ -32,14 +33,20 @@ with open( HC.LOGS_DIR + os.path.sep + 'client.log', 'a' ) as f:
try:
print( 'hydrus client started at ' + time.ctime() )
threading.Thread( target = reactor.run, kwargs = { 'installSignalHandlers' : 0 } ).start()
app = ClientController.Controller()
app.MainLoop()
print( 'hydrus client shut down at ' + time.ctime() )
except:
print( 'hydrus client failed at ' + time.ctime() )
import traceback
print( traceback.format_exc() )

View File

@ -8,6 +8,20 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 119</h3></li>
<ul>
<li>fixed an overzealous error in v118 update code</li>
<li>new direct ffmpeg video decoder for webm</li>
<li>new system takes advantage of multi-core processors, increasing render speed</li>
<li>improved animated frame timings</li>
<li>fixed a number of gifs that were causing the "None has no 'shape' attribute" error</li>
<li>sped up memory and subprocess cleanup after video playback</li>
<li>added some new ffmpeg instructions to the running from source help page</li>
<li>added client and server startup and shutdown statements to the log</li>
<li>improved some idle time logic, which should improve maintenance timings</li>
<li>fixed a critical idle timing bug that was adding 1s to many actions, including file queries!</li>
<li>fixed a pubsub testing bug that was preventing daemons from quickly closing after a unit test</li>
</ul>
<li><h3>version 118</h3></li>
<ul>
<li>improved animated frame buffer size calculation, saving memory and improving smaller animation performance</li>

View File

@ -16,18 +16,18 @@
<p>This works very like hydrus repository synchronisation, in the background, reporting its progress in the middle of the status bar at the bottom of the main window, like so:</p>
<p><img src="subs_status.png" /></p>
<p>You don't really have to care about this all that much; it just lets you know what it is doing.</p>
<p>Errors will be recovered from as gracefully as possible. The client will retry that subscription the next day.</p>
<p>Serious errors, like server 404s, are recovered from as gracefully as possible. The client will retry those subscriptions the next day.</p>
<p>Here's the result of the subscription I set up above:</p>
<p><img src="subs_import_done.png" /></p>
<p>It took about two minutes to download all that, and it all happened quietly in the background. Notice the 146 pending tags, up top.</p>
<h3>how could this possibly go wrong?</h3>
<p>This is quite a powerful tool, and if you are silly, you will end up spamming a server and likely upsetting someone or breaking something.</p>
<p>To initialise a subscription, the daemon will parse every single gallery and image page for that particular search. This is fine for the example above, which had 4 gallery pages and 73 image pages, but the search "short hair" on safebooru has about 6,400 gallery pages encompassing >250,000 results! Trying a search like that will take a tremendous amount of time for you and cause a non-trivial CPU and data hit to their server.</p>
<p>To initialise a subscription, the client will parse every single currently existing gallery and image page for that particular search. This is fine for the example above, which had 4 gallery pages and 73 image pages, but the search "short hair" on safebooru has about 6,400 gallery pages encompassing >250,000 results! Trying a search like that will take a tremendous amount of time for you and cause a non-trivial CPU and data hit to their server.</p>
<p><i>Remember: If you are going to scrape anything from a site, be polite about it!</i></p>
<p>So, I advise you start with artist searches to begin with. These usually top out at about 1,000 files, and hence don't take all that long. Once you are more confident, try doing multiple-tag queries. I suggest you leave simple single-tag queries for the manual download page, where you can hit 'that's enough' after ten or twenty pages.</p>
<h3>help! it won't stop!</h3>
<p>If you <i>do</i> put in a huge search, and the 'found x new files for subscription y' message is climbing terrifyingly higher and higher with no end in sight, you can pause the subscriptions daemon with <i>services->pause->subscriptions synchronisation</i>.</p>
<p>This will give you a breather to edit your subscriptions in the dialog. Remember to unpause when you are done.</p>
<p>This will give you a breather to reedit your subscriptions in the dialog. Remember to unpause when you are done.</p>
<p class="right"><a href="index.html">Go back to the index ---></a></p>
</div>
</body>

View File

@ -29,6 +29,7 @@
<p>For Windows, you can do the same thing with easy_install and pip and module installers. Since it doesn't natively come with Python, you'll probably need to go through a bigger list. You'll find <a href="http://www.lfd.uci.edu/~gohlke/pythonlibs/">this</a> page very helpful. I have a fair bit of experience with Windows python, so send me a mail if you need help.</a>
<p>Some people have encountered problems with wxPython 3.0, so I use 2.9.x in my official releases. Again, YMMV.</p>
<p>You'll probably want to install Pillow (pip should do it for Linux/OS X, easy_install for Windows) instead of PIL, but go back to PIL if Pillow doesn't work.</p>
<p>If you want to import videos, you will need to put a static <a href="http://ffmpeg.org/">FFMPEG</a> executable in the install_dir/bin directory. Have a look at how I do it in the extractable compiled releases if you can't figure it out. You can either copy the exe from one of those releases, or download it right from the FFMPEG site. I don't include the FFMPEG exes in the source release like I do upnpc just because they are so big.</a>
<p>Once you have everything set up, client.pyw and server.pyw should look for and run off the database files just like the executables.</p>
<p>I develop hydrus on 64-bit Win 7, so the program is much more stable and reasonable on Windows. I am developing for Linux and OS X, but I do not have as much experience with them, so I would particularly appreciate your Linux/OS X bug reports and any informed suggestions.</p>
<h3>my code</h3>

View File

@ -122,10 +122,10 @@ The database will be locked while the backup occurs, which may lock up your gui
def EventPubSub( self, event ):
HC.busy_doing_pubsub = True
HC.currently_doing_pubsub = True
try: HC.pubsub.WXProcessQueueItem()
finally: HC.busy_doing_pubsub = False
finally: HC.currently_doing_pubsub = False
def GetFullscreenImageCache( self ): return self._fullscreen_image_cache
@ -331,7 +331,7 @@ The database will be locked while the backup occurs, which may lock up your gui
def Read( self, action, *args, **kwargs ):
self._last_idle_time = HC.GetNow()
if action == 'media_results': self._last_idle_time = HC.GetNow()
return self._Read( action, *args, **kwargs )
@ -506,7 +506,7 @@ Once it is done, the client will restart.'''
def TIMEREventMaintenance( self, event ):
del gc.garbage[:]
gc.collect()
if self.CurrentlyIdle(): self.MaintainDB()
@ -518,12 +518,7 @@ Once it is done, the client will restart.'''
while True:
if HC.shutdown: raise Exception( 'Client shutting down!' )
elif HC.pubsub.NotBusy() and not HC.busy_doing_pubsub:
if not self.CurrentlyIdle(): time.sleep( 1 )
return
elif HC.pubsub.NoJobsQueued() and not HC.currently_doing_pubsub: return
else: time.sleep( 0.0001 )

View File

@ -4897,13 +4897,17 @@ class DB( ServiceDB ):
hash = filename.decode( 'hex' )
phash = HydrusImageHandling.GeneratePerceptualHash( path )
hash_id = self._GetHashId( c, hash )
c.execute( 'INSERT OR REPLACE INTO perceptual_hashes ( hash_id, phash ) VALUES ( ?, ? );', ( hash_id, sqlite3.Binary( phash ) ) )
i += 1
try:
phash = HydrusImageHandling.GeneratePerceptualHash( path )
hash_id = self._GetHashId( c, hash )
c.execute( 'INSERT OR REPLACE INTO perceptual_hashes ( hash_id, phash ) VALUES ( ?, ? );', ( hash_id, sqlite3.Binary( phash ) ) )
i += 1
except: print( 'When updating to v118, ' + path + '\'s phash could not be recalculated.' )
if i % 100 == 0: HC.app.SetSplashText( 'reprocessing thumbs: ' + HC.ConvertIntToPrettyString( i ) )

View File

@ -5,6 +5,7 @@ import ClientGUIDialogs
import ClientGUIDialogsManage
import ClientGUIMixins
import collections
import gc
import HydrusImageHandling
import HydrusVideoHandling
import os
@ -78,8 +79,6 @@ def GetExtraDimensions( media ):
class Animation( wx.Window ):
UPDATE_MS = 16
def __init__( self, parent, media, initial_size, initial_position ):
wx.Window.__init__( self, parent, size = initial_size, pos = initial_position )
@ -110,7 +109,12 @@ class Animation( wx.Window ):
self.EventResize( None )
self._timer_video.Start( self.UPDATE_MS, wx.TIMER_CONTINUOUS )
self._timer_video.Start( 16, wx.TIMER_ONE_SHOT )
def __del__( self ):
wx.CallLater( 500, gc.collect )
def _DrawFrame( self ):
@ -140,7 +144,11 @@ class Animation( wx.Window ):
self._current_frame_drawn = True
self._current_frame_drawn_at = max( time.clock(), self._current_frame_drawn_at + ( self._video_container.GetDuration( self._current_frame_index ) / 1000 ) )
now_in_ms = time.clock()
frame_was_supposed_to_be_at = self._current_frame_drawn_at + ( self._video_container.GetDuration( self._current_frame_index ) / 1000 )
if 1000.0 * ( now_in_ms - frame_was_supposed_to_be_at ) > 16.7: self._current_frame_drawn_at = now_in_ms
else: self._current_frame_drawn_at = frame_was_supposed_to_be_at
def _DrawWhite( self ):
@ -227,13 +235,15 @@ class Animation( wx.Window ):
def TIMEREventVideo( self, event ):
MIN_TIMER_TIME = 4
if self.IsShown():
if self._current_frame_drawn:
ms_since_current_frame_drawn = int( 1000.0 * ( time.clock() - self._current_frame_drawn_at ) )
time_to_update = ms_since_current_frame_drawn + float( self.UPDATE_MS ) / 2 > self._video_container.GetDuration( self._current_frame_index )
time_to_update = ms_since_current_frame_drawn + MIN_TIMER_TIME / 2 > self._video_container.GetDuration( self._current_frame_index )
if not self._paused and time_to_update:
@ -249,6 +259,12 @@ class Animation( wx.Window ):
if not self._current_frame_drawn and self._video_container.HasFrame( self._current_frame_index ): self._DrawFrame()
ms_since_current_frame_drawn = int( 1000.0 * ( time.clock() - self._current_frame_drawn_at ) )
ms_until_next_frame = max( MIN_TIMER_TIME, self._video_container.GetDuration( self._current_frame_index ) - ms_since_current_frame_drawn )
self._timer_video.Start( ms_until_next_frame, wx.TIMER_ONE_SHOT )
class AnimationBar( wx.Window ):

View File

@ -64,7 +64,7 @@ options = {}
# Misc
NETWORK_VERSION = 13
SOFTWARE_VERSION = 118
SOFTWARE_VERSION = 119
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )
@ -84,7 +84,7 @@ subs_changed = False
http = None
busy_doing_pubsub = False
currently_doing_pubsub = False
# Enums

View File

@ -1135,7 +1135,7 @@ class ImportArgsGeneratorGallery( ImportArgsGenerator ):
if status == 'redundant':
( media_result, ) = HC.app.Read( 'media_results', HC.LOCAL_FILE_SERVICE_IDENTIFIER, ( hash, ) )
( media_result, ) = HC.app.ReadDaemon( 'media_results', HC.LOCAL_FILE_SERVICE_IDENTIFIER, ( hash, ) )
do_tags = len( self._advanced_tag_options ) > 0
@ -1263,7 +1263,7 @@ class ImportArgsGeneratorThread( ImportArgsGenerator ):
if status == 'redundant':
( media_result, ) = HC.app.Read( 'media_results', HC.LOCAL_FILE_SERVICE_IDENTIFIER, ( hash, ) )
( media_result, ) = HC.app.ReadDaemon( 'media_results', HC.LOCAL_FILE_SERVICE_IDENTIFIER, ( hash, ) )
return ( status, media_result )
@ -1303,7 +1303,7 @@ class ImportArgsGeneratorURLs( ImportArgsGenerator ):
if status == 'redundant':
( media_result, ) = HC.app.Read( 'media_results', HC.LOCAL_FILE_SERVICE_IDENTIFIER, ( hash, ) )
( media_result, ) = HC.app.ReadDaemon( 'media_results', HC.LOCAL_FILE_SERVICE_IDENTIFIER, ( hash, ) )
return ( status, media_result )

View File

@ -523,7 +523,7 @@ class HydrusBitmap():
def GetSize( self ): return self._size
class RasterContainer():
class RasterContainer( object ):
def __init__( self, media, target_resolution = None ):

View File

@ -60,7 +60,7 @@ class HydrusPubSub():
return callables
def NotBusy( self ):
def NoJobsQueued( self ):
with self._lock:
@ -131,7 +131,7 @@ class HydrusPubSub():
def WXpubimmediate( self, topic, *args, **kwargs ):
with self._lock:
callables = self._GetCallables( topic )
for callable in callables: callable( *args, **kwargs )

View File

@ -140,6 +140,8 @@ class DAEMONCallToThread( DAEMON ):
callable( *args, **kwargs )
del callable
except Exception as e: HC.ShowException( e )
time.sleep( 0.00001 )

View File

@ -7,12 +7,19 @@ import HydrusExceptions
import HydrusImageHandling
import HydrusThreading
import matroska
import numpy
import os
import re
import subprocess
import traceback
import threading
import time
from wx import wx
if HC.PLATFORM_LINUX: FFMPEG_PATH = '"' + HC.BIN_DIR + os.path.sep + 'ffmpeg"'
elif HC.PLATFORM_OSX: FFMPEG_PATH = '"' + HC.BIN_DIR + os.path.sep + 'ffmpeg"'
elif HC.PLATFORM_WINDOWS: FFMPEG_PATH = '"' + HC.BIN_DIR + os.path.sep + 'ffmpeg.exe"'
def GetCVVideoProperties( path ):
capture = cv2.VideoCapture( path )
@ -120,6 +127,247 @@ def GetMatroskaOrWebMProperties( path ):
return ( ( width, height ), duration, num_frames )
# This is cribbed from moviepy's FFMPEG_VideoReader
class HydrusFFMPEG_VideoReader( object ):
def __init__(self, media, print_infos=False, bufsize = None, pix_fmt="rgb24"):
self._media = media
hash = self._media.GetHash()
mime = self._media.GetMime()
self._path = CC.GetFilePath( hash, mime )
self.size = self._media.GetResolution()
self.duration = float( self._media.GetDuration() ) / 1000.0
self.nframes = self._media.GetNumFrames()
self.fps = float( self.nframes ) / self.duration
self.pix_fmt = pix_fmt
if pix_fmt == 'rgba': self.depth = 4
else: self.depth = 3
if bufsize is None:
( x, y ) = self.size
bufsize = self.depth * x * y * 5
self.process = None
self.bufsize = bufsize
self.initialize()
def __del__( self ):
self.close()
def close(self):
if self.process is not None:
self.process.terminate()
self.process.stdout.close()
self.process.stderr.close()
self.process = None
def initialize( self, starttime=0 ):
"""Opens the file, creates the pipe. """
self.close()
if starttime != 0:
offset = min( 1, starttime )
i_arg = [ '-ss', "%.03f" % ( starttime - offset ), '-i', '"' + self._path + '"' ]
else: i_arg = [ '-i', '"' + self._path + '"' ]
cmd = ([FFMPEG_PATH]+ i_arg +
['-loglevel', 'error',
'-f', 'image2pipe',
"-pix_fmt", self.pix_fmt,
'-vcodec', 'rawvideo', '-'])
self.process = subprocess.Popen( ' '.join( cmd ), shell = True, bufsize= self.bufsize, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
self.pos = int( round( self.fps * starttime ) )
def skip_frames(self, n=1):
"""Reads and throws away n frames """
( w, h ) = self.size
for i in range( n ):
self.process.stdout.read( self.depth * w * h )
self.process.stdout.flush()
self.pos += n
def read_frame( self ):
( w, h ) = self.size
nbytes = self.depth * w * h
s = self.process.stdout.read(nbytes)
self.pos += 1
if len(s) != nbytes:
print( "Warning: in file %s, "%(self._path)+
"%d bytes wanted but %d bytes read,"%(nbytes, len(s))+
"at frame %d/%d, at time %.02f/%.02f sec. "%(
self.pos,self.nframes,
1.0*self.pos/self.fps,
self.duration)+
"Using the last valid frame instead.")
result = self.lastread
self.close()
else:
result = numpy.fromstring( s, dtype = 'uint8' ).reshape( ( h, w, len( s ) // ( w * h ) ) )
self.lastread = result
return result
def set_position( self, pos ):
if ( pos < self.pos ) or ( pos > self.pos + 60 ):
starttime = float( pos ) / self.fps
self.initialize( starttime )
else: self.skip_frames( pos - self.pos )
# same thing; this is cribbed from moviepy
def Hydrusffmpeg_parse_infos(filename, print_infos=False):
"""Get file infos using ffmpeg.
Returns a dictionnary with the fields:
"video_found", "video_fps", "duration", "video_nframes",
"video_duration"
"audio_found", "audio_fps"
"video_duration" is slightly smaller than "duration" to avoid
fetching the uncomplete frames at the end, which raises an error.
"""
# open the file in a pipe, provoke an error, read output
cmd = [FFMPEG_PATH, "-i", '"' + filename + '"']
is_GIF = filename.endswith('.gif')
if is_GIF:
if HC.PLATFORM_WINDOWS: cmd += ["-f", "null", "NUL"]
else: cmd += ["-f", "null", "/dev/null"]
proc = subprocess.Popen( ' '.join( cmd ), shell = True, bufsize=10**5, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
infos = proc.stderr.read().decode('utf8')
proc.terminate()
del proc
if print_infos:
# print the whole info text returned by FFMPEG
print( infos )
lines = infos.splitlines()
if "No such file or directory" in lines[-1]:
raise IOError("%s not found ! Wrong path ?"%filename)
result = dict()
# get duration (in seconds)
try:
keyword = ('frame=' if is_GIF else 'Duration: ')
line = [l for l in lines if keyword in l][0]
match = re.search("[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]", line)
hms = map(float, line[match.start()+1:match.end()].split(':'))
if len(hms) == 1:
result['duration'] = hms[0]
elif len(hms) == 2:
result['duration'] = 60*hms[0]+hms[1]
elif len(hms) ==3:
result['duration'] = 3600*hms[0]+60*hms[1]+hms[2]
except:
raise IOError("Error reading duration in file %s,"%(filename)+
"Text parsed: %s"%infos)
# get the output line that speaks about video
lines_video = [l for l in lines if ' Video: ' in l]
result['video_found'] = ( lines_video != [] )
if result['video_found']:
line = lines_video[0]
# get the size, of the form 460x320 (w x h)
match = re.search(" [0-9]*x[0-9]*(,| )", line)
s = list(map(int, line[match.start():match.end()-1].split('x')))
result['video_size'] = s
# get the frame rate
try:
match = re.search("( [0-9]*.| )[0-9]* tbr", line)
result['video_fps'] = float(line[match.start():match.end()].split(' ')[1])
except:
match = re.search("( [0-9]*.| )[0-9]* fps", line)
result['video_fps'] = float(line[match.start():match.end()].split(' ')[1])
result['video_nframes'] = int(result['duration']*result['video_fps'])+1
result['video_duration'] = result['duration']
# We could have also recomputed the duration from the number
# of frames, as follows:
# >>> result['video_duration'] = result['video_nframes'] / result['video_fps']
lines_audio = [l for l in lines if ' Audio: ' in l]
result['audio_found'] = lines_audio != []
if result['audio_found']:
line = lines_audio[0]
try:
match = re.search(" [0-9]* Hz", line)
result['audio_fps'] = int(line[match.start()+1:match.end()])
except:
result['audio_fps'] = 'unknown'
return result
class VideoContainer( HydrusImageHandling.RasterContainer ):
BUFFER_SIZE = 1024 * 1024 * 96
@ -143,7 +391,7 @@ class VideoContainer( HydrusImageHandling.RasterContainer ):
if self._media.GetMime() == HC.IMAGE_GIF: self._durations = HydrusImageHandling.GetGIFFrameDurations( self._path )
else: self._frame_duration = GetVideoFrameDuration( self._path )
self._renderer = VideoRenderer( self, self._media, self._target_resolution )
self._renderer = VideoRendererMoviePy( self, self._media, self._target_resolution )
num_frames = self.GetNumFrames()
@ -231,16 +479,16 @@ class VideoContainer( HydrusImageHandling.RasterContainer ):
self._maximum_frame_asked_for = new_maximum_frame_to_ask_for
self._renderer.SetRenderToPosition( self._maximum_frame_asked_for)
self._renderer.SetRenderToPosition( self._maximum_frame_asked_for )
else:
else: # index > self._last_index_asked_for
no_wraparound = self._minimum_frame_asked_for < self._maximum_frame_asked_for
currently_no_wraparound = self._minimum_frame_asked_for < self._maximum_frame_asked_for
self._minimum_frame_asked_for = new_minimum_frame_to_ask_for
if no_wraparound:
if currently_no_wraparound:
if index > self._maximum_frame_asked_for:
@ -268,8 +516,9 @@ class VideoContainer( HydrusImageHandling.RasterContainer ):
self._MaintainBuffer()
class VideoRenderer():
class VideoRendererCV():
def __init__( self, image_container, media, target_resolution ):
@ -419,4 +668,142 @@ class VideoRenderer():
class VideoRendererMoviePy():
def __init__( self, image_container, media, target_resolution ):
self._image_container = image_container
self._media = media
self._target_resolution = target_resolution
self._lock = threading.Lock()
( x, y ) = media.GetResolution()
self._ffmpeg_reader = HydrusFFMPEG_VideoReader( media, bufsize = x * y * 3 * 5 )
self._current_index = 0
self._last_index_rendered = -1
self._last_frame = None
self._render_to_index = -1
def _GetCurrentFrame( self ):
try: image = self._ffmpeg_reader.read_frame()
except Exception as e: raise HydrusExceptions.CantRenderWithCVException( 'FFMPEG could not render frame ' + HC.u( self._current_index ) + '.' + os.linesep * 2 + HC.u( e ) )
self._last_index_rendered = self._current_index
num_frames = self._media.GetNumFrames()
self._current_index = ( self._current_index + 1 ) % num_frames
if self._current_index == 0 and self._last_index_rendered != 0: self._ffmpeg_reader.initialize()
return image
def _RenderCurrentFrame( self ):
try:
image = self._GetCurrentFrame()
image = HydrusImageHandling.EfficientlyResizeCVImage( image, self._target_resolution )
self._last_frame = image
except HydrusExceptions.CantRenderWithCVException:
if self._last_frame is None: raise
image = self._last_frame
return HydrusImageHandling.GenerateHydrusBitmapFromCVImage( image )
def _RenderFrames( self ):
no_frames_yet = True
while True:
try:
yield self._RenderCurrentFrame()
no_frames_yet = False
except HydrusExceptions.CantRenderWithCVException:
if no_frames_yet: raise
else: break
def SetRenderToPosition( self, index ):
with self._lock:
if self._render_to_index != index:
self._render_to_index = index
HydrusThreading.CallToThread( self.THREADDoWork )
def SetPosition( self, index ):
with self._lock:
if index == self._current_index: return
else:
if self._media.GetMime() == HC.IMAGE_GIF:
if index < self._current_index:
self._ffmpeg_reader.initialize()
self._ffmpeg_reader.skip_frames( index )
else:
timecode = float( index ) / self._ffmpeg_reader.fps
self._ffmpeg_reader.set_position( timecode )
self._render_to_index = index
def THREADDoWork( self ):
while True:
time.sleep( 0.00001 ) # thread yield
with self._lock:
if self._last_index_rendered != self._render_to_index:
index = self._current_index
frame = self._RenderCurrentFrame()
wx.CallAfter( self._image_container.AddFrame, index, frame )
else: break

View File

@ -5,6 +5,7 @@ import HydrusTags
import os
import random
import threading
import weakref
tinest_gif = '\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x00\xFF\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00\x3B'
@ -60,14 +61,58 @@ class FakePubSub():
self._pubsubs = collections.defaultdict( list )
self._callables = []
self._lock = threading.Lock()
self._topics_to_objects = {}
self._topics_to_method_names = {}
def _GetCallables( self, topic ):
callables = []
if topic in self._topics_to_objects:
try:
objects = self._topics_to_objects[ topic ]
for object in objects:
method_names = self._topics_to_method_names[ topic ]
for method_name in method_names:
if hasattr( object, method_name ):
try:
callable = getattr( object, method_name )
callables.append( callable )
except wx.PyDeadObjectError: pass
except TypeError as e:
if '_wxPyDeadObject' not in str( e ): raise
except: pass
return callables
def ClearPubSubs( self ): self._pubsubs = collections.defaultdict( list )
def GetPubSubs( self, topic ): return self._pubsubs[ topic ]
def NotBusy( self ): return True
def NoJobsQueued( self ): return True
def WXProcessQueueItem( self ): pass
@ -79,13 +124,22 @@ class FakePubSub():
def sub( self, object, method_name, topic ): pass
def sub( self, object, method_name, topic ):
if topic not in self._topics_to_objects: self._topics_to_objects[ topic ] = weakref.WeakSet()
if topic not in self._topics_to_method_names: self._topics_to_method_names[ topic ] = set()
self._topics_to_objects[ topic ].add( object )
self._topics_to_method_names[ topic ].add( method_name )
def WXpubimmediate( self, topic, *args, **kwargs ):
with self._lock:
self._pubsubs[ topic ].append( ( args, kwargs ) )
callables = self._GetCallables( topic )
for callable in callables: callable( *args, **kwargs )

View File

@ -17,6 +17,7 @@ from include import HydrusConstants as HC
import os
import sys
import time
from include import ServerController
import threading
from twisted.internet import reactor
@ -31,14 +32,20 @@ with open( HC.LOGS_DIR + os.path.sep + 'server.log', 'a' ) as f:
try:
print( 'hydrus server started at ' + time.ctime() )
threading.Thread( target = reactor.run, kwargs = { 'installSignalHandlers' : 0 } ).start()
app = ServerController.Controller()
app.MainLoop()
print( 'hydrus server shut down at ' + time.ctime() )
except:
print( 'hydrus server failed at ' + time.ctime() )
import traceback
print( traceback.format_exc() )

View File

@ -63,6 +63,7 @@ class App( wx.App ):
self._managers[ 'tag_censorship' ] = HydrusTags.TagCensorshipManager()
self._managers[ 'tag_siblings' ] = HydrusTags.TagSiblingsManager()
self._managers[ 'tag_parents' ] = HydrusTags.TagParentsManager()
self._managers[ 'undo' ] = CC.UndoManager()
self._managers[ 'web_sessions' ] = TestConstants.FakeWebSessionManager()
@ -150,11 +151,14 @@ if __name__ == '__main__':
else: only_run = None
old_pubsub = HC.pubsub
app = App()
HC.shutdown = True
HC.pubsub.WXpubimmediate( 'shutdown' )
raw_input()
old_pubsub.WXpubimmediate( 'shutdown' )
raw_input()