hydrus/include/ClientController.py

585 lines
20 KiB
Python
Raw Normal View History

2014-04-23 20:56:12 +00:00
import collections
2013-02-19 00:11:43 +00:00
import gc
2013-11-06 18:22:07 +00:00
import hashlib
2013-09-25 20:20:10 +00:00
import httplib
2013-02-19 00:11:43 +00:00
import HydrusConstants as HC
2013-07-24 20:26:00 +00:00
import HydrusExceptions
2014-01-29 21:59:42 +00:00
import HydrusNetworking
2013-03-15 02:38:12 +00:00
import HydrusSessions
2013-09-25 20:20:10 +00:00
import HydrusServer
2013-07-17 20:56:13 +00:00
import HydrusTags
2014-05-21 21:37:35 +00:00
import HydrusThreading
2013-02-19 00:11:43 +00:00
import ClientConstants as CC
import ClientDB
import ClientGUI
2013-05-29 20:19:54 +00:00
import ClientGUIDialogs
2013-02-19 00:11:43 +00:00
import os
2013-08-28 21:31:52 +00:00
import random
import shutil
2013-04-03 20:56:07 +00:00
import sqlite3
2014-01-01 20:01:00 +00:00
import stat
2014-01-08 18:40:02 +00:00
import subprocess
2013-07-24 20:26:00 +00:00
import sys
2013-02-19 00:11:43 +00:00
import threading
import time
import traceback
import wx
import wx.richtext
2013-10-02 22:06:06 +00:00
from twisted.internet import reactor
2013-10-23 21:36:47 +00:00
from twisted.internet import defer
2013-02-19 00:11:43 +00:00
ID_ANIMATED_EVENT_TIMER = wx.NewId()
ID_MAINTENANCE_EVENT_TIMER = wx.NewId()
2014-08-13 22:18:12 +00:00
MAINTENANCE_PERIOD = 5 * 60
2014-08-06 20:29:17 +00:00
2013-02-19 00:11:43 +00:00
class Controller( wx.App ):
2013-08-14 20:21:49 +00:00
def _Read( self, action, *args, **kwargs ): return self._db.Read( action, HC.HIGH_PRIORITY, *args, **kwargs )
2013-05-08 20:31:00 +00:00
2013-07-31 21:26:38 +00:00
def _Write( self, action, priority, synchronous, *args, **kwargs ): return self._db.Write( action, priority, synchronous, *args, **kwargs )
2013-05-08 20:31:00 +00:00
2014-01-08 18:40:02 +00:00
def BackupDatabase( self ):
with wx.DirDialog( self._gui, 'Select backup location.' ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
message = '''Are you sure "''' + path + '''" is the correct directory?
Everything already in that directory will be deleted before the backup starts.
The database will be locked while the backup occurs, which may lock up your gui as well.'''
with ClientGUIDialogs.DialogYesNo( self._gui, message ) as dlg_yn:
if dlg_yn.ShowModal() == wx.ID_YES:
self.Write( 'backup', path )
2013-02-19 00:11:43 +00:00
def ClearCaches( self ):
self._thumbnail_cache.Clear()
self._fullscreen_image_cache.Clear()
self._preview_image_cache.Clear()
def Clipboard( self, type, data ):
# need this cause can't do it in a non-gui thread
if type == 'paths':
paths = data
if wx.TheClipboard.Open():
2013-03-15 02:38:12 +00:00
data = wx.DataObjectComposite()
2013-02-19 00:11:43 +00:00
2013-03-15 02:38:12 +00:00
file_data = wx.FileDataObject()
for path in paths: file_data.AddFile( path )
text_data = wx.TextDataObject( os.linesep.join( paths ) )
data.Add( file_data, True )
data.Add( text_data, False )
2013-02-19 00:11:43 +00:00
wx.TheClipboard.SetData( data )
wx.TheClipboard.Close()
2013-03-15 02:38:12 +00:00
else: wx.MessageBox( 'Could not get permission to access the clipboard!' )
2013-02-19 00:11:43 +00:00
2013-03-23 17:57:29 +00:00
elif type == 'text':
text = data
if wx.TheClipboard.Open():
data = wx.TextDataObject( text )
wx.TheClipboard.SetData( data )
wx.TheClipboard.Close()
else: wx.MessageBox( 'I could not get permission to access the clipboard.' )
2013-02-19 00:11:43 +00:00
2014-08-13 22:18:12 +00:00
def CurrentlyIdle( self ): return HC.GetNow() - self._timestamps[ 'last_user_action' ] > 30 * 60 # 30 mins since last canvas media swap
2014-02-19 22:37:23 +00:00
2013-02-19 00:11:43 +00:00
def EventPubSub( self, event ):
2014-06-18 21:53:48 +00:00
HC.currently_doing_pubsub = True
2013-08-28 21:31:52 +00:00
2013-11-13 21:30:38 +00:00
try: HC.pubsub.WXProcessQueueItem()
2014-06-18 21:53:48 +00:00
finally: HC.currently_doing_pubsub = False
2013-02-19 00:11:43 +00:00
2014-08-06 20:29:17 +00:00
def GetDB( self ): return self._db
2013-02-19 00:11:43 +00:00
def GetFullscreenImageCache( self ): return self._fullscreen_image_cache
def GetGUI( self ): return self._gui
def GetLog( self ): return self._log
2013-11-27 18:27:11 +00:00
def GetManager( self, type ): return self._managers[ type ]
2013-10-09 18:13:42 +00:00
2013-02-19 00:11:43 +00:00
def GetPreviewImageCache( self ): return self._preview_image_cache
def GetThumbnailCache( self ): return self._thumbnail_cache
2014-08-06 20:29:17 +00:00
def InitCheckPassword( self ):
while True:
with wx.PasswordEntryDialog( None, 'Enter your password', 'Enter password' ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
if hashlib.sha256( dlg.GetValue() ).digest() == HC.options[ 'password' ]: break
else: raise HydrusExceptions.PermissionException()
def InitDB( self ):
self._log = CC.Log()
try:
def make_temp_files_deletable( function_called, path, traceback_gumpf ):
os.chmod( path, stat.S_IWRITE )
function_called( path ) # try again
if os.path.exists( HC.TEMP_DIR ): shutil.rmtree( HC.TEMP_DIR, onerror = make_temp_files_deletable )
except: pass
try:
if not os.path.exists( HC.TEMP_DIR ): os.mkdir( HC.TEMP_DIR )
except: pass
db_initialised = False
while not db_initialised:
try:
self._db = ClientDB.DB()
db_initialised = True
except HydrusExceptions.DBAccessException as e:
try: print( HC.u( e ) )
except: print( repr( HC.u( e ) ) )
message = 'This instance of the client had a problem connecting to the database, which probably means an old instance is still closing.'
message += os.linesep + os.linesep
message += 'If the old instance does not close for a _very_ long time, you can usually safely force-close it from task manager.'
with ClientGUIDialogs.DialogYesNo( None, message, yes_label = 'wait a bit, then try again', no_label = 'forget it' ) as dlg:
if dlg.ShowModal() == wx.ID_YES: time.sleep( 3 )
else: raise HydrusExceptions.PermissionException()
threading.Thread( target = self._db.MainLoop, name = 'Database Main Loop' ).start()
def InitGUI( self ):
self._managers = {}
2014-08-27 22:15:22 +00:00
self._managers[ 'services' ] = CC.ServicesManager()
2014-08-13 22:18:12 +00:00
2014-08-06 20:29:17 +00:00
self._managers[ 'hydrus_sessions' ] = HydrusSessions.HydrusSessionManagerClient()
self._managers[ 'local_booru' ] = CC.LocalBooruCache()
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' ] = HydrusSessions.WebSessionManagerClient()
self._fullscreen_image_cache = CC.RenderedImageCache( 'fullscreen' )
self._preview_image_cache = CC.RenderedImageCache( 'preview' )
self._thumbnail_cache = CC.ThumbnailCache()
CC.GlobalBMPs.STATICInitialise()
self._gui = ClientGUI.FrameGUI()
HC.pubsub.sub( self, 'Clipboard', 'clipboard' )
HC.pubsub.sub( self, 'RestartServer', 'restart_server' )
HC.pubsub.sub( self, 'RestartBooru', 'restart_booru' )
self.Bind( wx.EVT_TIMER, self.TIMEREventMaintenance, id = ID_MAINTENANCE_EVENT_TIMER )
self._maintenance_event_timer = wx.Timer( self, ID_MAINTENANCE_EVENT_TIMER )
self._maintenance_event_timer.Start( MAINTENANCE_PERIOD * 1000, wx.TIMER_CONTINUOUS )
# this is because of some bug in wx C++ that doesn't add these by default
wx.richtext.RichTextBuffer.AddHandler( wx.richtext.RichTextHTMLHandler() )
wx.richtext.RichTextBuffer.AddHandler( wx.richtext.RichTextXMLHandler() )
if HC.is_first_start: wx.CallAfter( self._gui.DoFirstStart )
if HC.is_db_updated: wx.CallLater( 1, HC.ShowText, 'The client has updated to version ' + HC.u( HC.SOFTWARE_VERSION ) + '!' )
self.RestartServer()
self.RestartBooru()
self._db.StartDaemons()
2013-02-19 00:11:43 +00:00
def MaintainDB( self ):
2013-07-24 20:26:00 +00:00
sys.stdout.flush()
sys.stderr.flush()
2013-07-10 20:25:57 +00:00
gc.collect()
2013-07-31 21:26:38 +00:00
now = HC.GetNow()
2013-02-19 00:11:43 +00:00
shutdown_timestamps = self.Read( 'shutdown_timestamps' )
if now - shutdown_timestamps[ CC.SHUTDOWN_TIMESTAMP_VACUUM ] > 86400 * 5: self.Write( 'vacuum' )
if now - shutdown_timestamps[ CC.SHUTDOWN_TIMESTAMP_DELETE_ORPHANS ] > 86400 * 3: self.Write( 'delete_orphans' )
2014-08-06 20:29:17 +00:00
if now - self._timestamps[ 'last_service_info_cache_fatten' ] > 60 * 20:
HC.pubsub.pub( 'set_splash_text', 'fattening service info' )
2014-04-23 20:56:12 +00:00
2014-08-13 22:18:12 +00:00
services = self.GetManager( 'services' ).GetServices()
2014-04-23 20:56:12 +00:00
2014-08-13 22:18:12 +00:00
for service in services: self.Read( 'service_info', service.GetKey() )
2014-04-23 20:56:12 +00:00
2014-08-06 20:29:17 +00:00
self._timestamps[ 'service_info_cache_fatten' ] = HC.GetNow()
HC.pubsub.pub( 'clear_closed_pages' )
2014-04-23 20:56:12 +00:00
2013-02-19 00:11:43 +00:00
def OnInit( self ):
2014-04-23 20:56:12 +00:00
2013-07-10 20:25:57 +00:00
HC.app = self
2014-01-29 21:59:42 +00:00
HC.http = HydrusNetworking.HTTPConnectionManager()
2013-07-10 20:25:57 +00:00
2014-04-23 20:56:12 +00:00
self._timestamps = collections.defaultdict( lambda: 0 )
self._timestamps[ 'boot' ] = HC.GetNow()
2013-09-25 20:20:10 +00:00
self._local_service = None
2014-07-09 22:15:14 +00:00
self._booru_service = None
2013-09-25 20:20:10 +00:00
2014-08-06 20:29:17 +00:00
self.Bind( HC.EVT_PUBSUB, self.EventPubSub )
2013-11-06 18:22:07 +00:00
2013-02-19 00:11:43 +00:00
try:
2014-08-06 20:29:17 +00:00
splash = ClientGUI.FrameSplash( 'boot' )
2013-02-19 00:11:43 +00:00
2014-08-06 20:29:17 +00:00
return True
2013-02-19 00:11:43 +00:00
except:
2014-08-06 20:29:17 +00:00
print( 'There was an error trying to start the splash screen!' )
2013-02-19 00:11:43 +00:00
2014-08-06 20:29:17 +00:00
print( traceback.format_exc() )
2013-02-19 00:11:43 +00:00
2014-08-06 20:29:17 +00:00
try: wx.CallAfter( splash.Destroy )
2014-01-22 21:11:22 +00:00
except: pass
2014-08-06 20:29:17 +00:00
return False
2013-02-19 00:11:43 +00:00
def PrepStringForDisplay( self, text ):
2013-08-14 20:21:49 +00:00
if HC.options[ 'gui_capitalisation' ]: return text
2013-02-19 00:11:43 +00:00
else: return text.lower()
2014-08-13 22:18:12 +00:00
def Read( self, action, *args, **kwargs ): return self._Read( action, *args, **kwargs )
2013-04-24 21:23:53 +00:00
2013-07-31 21:26:38 +00:00
def ReadDaemon( self, action, *args, **kwargs ):
result = self._Read( action, *args, **kwargs )
time.sleep( 0.1 )
return result
2013-04-24 21:23:53 +00:00
2014-08-13 22:18:12 +00:00
def ResetIdleTimer( self ): self._timestamps[ 'last_user_action' ] = HC.GetNow()
2014-07-09 22:15:14 +00:00
def RestartBooru( self ):
2014-08-27 22:15:22 +00:00
service = self.GetManager( 'services' ).GetService( HC.LOCAL_BOORU_SERVICE_KEY )
2014-07-09 22:15:14 +00:00
info = service.GetInfo()
port = info[ 'port' ]
def TWISTEDRestartServer():
def StartServer( *args, **kwargs ):
try:
connection = httplib.HTTPConnection( '127.0.0.1', port, timeout = 10 )
try:
connection.connect()
connection.close()
text = 'The client\'s booru server could not start because something was already bound to port ' + HC.u( port ) + '.'
text += os.linesep * 2
text += 'This usually means another hydrus client is already running and occupying that port. It could be a previous instantiation of this client that has yet to shut itself down.'
text += os.linesep * 2
text += 'You can change the port this client tries to host its local server on in services->manage services.'
wx.CallLater( 1, HC.ShowText, text )
except:
2014-08-27 22:15:22 +00:00
self._booru_service = reactor.listenTCP( port, HydrusServer.HydrusServiceBooru( HC.LOCAL_BOORU_SERVICE_KEY, HC.LOCAL_BOORU, 'This is the local booru.' ) )
2014-07-09 22:15:14 +00:00
connection = httplib.HTTPConnection( '127.0.0.1', port, timeout = 10 )
try:
connection.connect()
connection.close()
except:
text = 'Tried to bind port ' + HC.u( port ) + ' for the local booru, but it failed.'
wx.CallLater( 1, HC.ShowText, text )
except Exception as e:
wx.CallAfter( HC.ShowException, e )
if self._booru_service is None: StartServer()
else:
deferred = defer.maybeDeferred( self._booru_service.stopListening )
deferred.addCallback( StartServer )
reactor.callFromThread( TWISTEDRestartServer )
2013-09-25 20:20:10 +00:00
def RestartServer( self ):
2013-10-02 22:06:06 +00:00
port = HC.options[ 'local_port' ]
2013-09-25 20:20:10 +00:00
def TWISTEDRestartServer():
2013-10-23 21:36:47 +00:00
def StartServer( *args, **kwargs ):
2013-09-25 20:20:10 +00:00
try:
2013-10-02 22:06:06 +00:00
connection = httplib.HTTPConnection( '127.0.0.1', port, timeout = 10 )
2013-09-25 20:20:10 +00:00
2013-10-02 22:06:06 +00:00
try:
connection.connect()
connection.close()
2014-05-28 21:03:24 +00:00
text = 'The client\'s local server could not start because something was already bound to port ' + HC.u( port ) + '.'
text += os.linesep * 2
text += 'This usually means another hydrus client is already running and occupying that port. It could be a previous instantiation of this client that has yet to shut itself down.'
text += os.linesep * 2
text += 'You can change the port this client tries to host its local server on in file->options.'
2013-10-02 22:06:06 +00:00
2014-04-09 20:18:58 +00:00
wx.CallLater( 1, HC.ShowText, text )
2013-10-02 22:06:06 +00:00
except:
2014-08-27 22:15:22 +00:00
self._local_service = reactor.listenTCP( port, HydrusServer.HydrusServiceLocal( HC.LOCAL_FILE_SERVICE_KEY, HC.LOCAL_FILE, 'This is the local file service.' ) )
2013-10-02 22:06:06 +00:00
connection = httplib.HTTPConnection( '127.0.0.1', port, timeout = 10 )
try:
connection.connect()
connection.close()
except:
2014-07-09 22:15:14 +00:00
text = 'Tried to bind port ' + HC.u( port ) + ' for the local server, but it failed.'
2013-10-02 22:06:06 +00:00
2014-04-09 20:18:58 +00:00
wx.CallLater( 1, HC.ShowText, text )
2013-10-02 22:06:06 +00:00
except Exception as e:
wx.CallAfter( HC.ShowException, e )
2013-09-25 20:20:10 +00:00
if self._local_service is None: StartServer()
else:
2013-10-23 21:36:47 +00:00
deferred = defer.maybeDeferred( self._local_service.stopListening )
2013-09-25 20:20:10 +00:00
2013-10-23 21:36:47 +00:00
deferred.addCallback( StartServer )
2013-09-25 20:20:10 +00:00
2013-10-02 22:06:06 +00:00
reactor.callFromThread( TWISTEDRestartServer )
2013-09-25 20:20:10 +00:00
2014-01-08 18:40:02 +00:00
def RestoreDatabase( self ):
with wx.DirDialog( self._gui, 'Select backup location.' ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
message = '''Are you sure you want to restore a backup from "''' + path + '''"?
Everything in your current database will be deleted!
The gui will shut down, and then it will take a while to complete the restore.
Once it is done, the client will restart.'''
with ClientGUIDialogs.DialogYesNo( self._gui, message ) as dlg_yn:
if dlg_yn.ShowModal() == wx.ID_YES:
self._gui.Hide()
2014-01-22 21:11:22 +00:00
self._gui.Close()
2014-01-08 18:40:02 +00:00
self._db.Shutdown()
2014-08-06 20:29:17 +00:00
while not self._db.LoopIsFinished(): time.sleep( 0.1 )
2014-01-08 18:40:02 +00:00
self._db.RestoreBackup( path )
call_stuff = [ sys.executable ]
call_stuff.extend( sys.argv )
subprocess.call( call_stuff, shell = True )
2014-05-21 21:37:35 +00:00
def StartFileQuery( self, query_key, search_context ): HydrusThreading.CallToThread( self.THREADDoFileQuery, query_key, search_context )
2013-08-28 21:31:52 +00:00
def THREADDoFileQuery( self, query_key, search_context ):
try:
2014-08-13 22:18:12 +00:00
query_hash_ids = self.Read( 'file_query_ids', search_context )
2013-08-28 21:31:52 +00:00
query_hash_ids = list( query_hash_ids )
random.shuffle( query_hash_ids )
limit = search_context.GetSystemPredicates().GetLimit()
if limit is not None: query_hash_ids = query_hash_ids[ : limit ]
2014-08-27 22:15:22 +00:00
service_key = search_context.GetFileServiceKey()
2013-08-28 21:31:52 +00:00
include_current_tags = search_context.IncludeCurrentTags()
media_results = []
include_pending_tags = search_context.IncludePendingTags()
i = 0
base = 256
while i < len( query_hash_ids ):
if query_key.IsCancelled(): return
if i == 0: ( last_i, i ) = ( 0, base )
else: ( last_i, i ) = ( i, i + base )
sub_query_hash_ids = query_hash_ids[ last_i : i ]
2014-08-27 22:15:22 +00:00
more_media_results = self.Read( 'media_results_from_ids', service_key, sub_query_hash_ids )
2013-08-28 21:31:52 +00:00
media_results.extend( more_media_results )
HC.pubsub.pub( 'set_num_query_results', len( media_results ), len( query_hash_ids ) )
2014-08-13 22:18:12 +00:00
self.WaitUntilGoodTimeToUseGUIThread()
2013-08-28 21:31:52 +00:00
HC.pubsub.pub( 'file_query_done', query_key, media_results )
except Exception as e: HC.ShowException( e )
2014-05-28 21:03:24 +00:00
def TIMEREventMaintenance( self, event ):
2014-02-05 20:54:28 +00:00
2014-08-06 20:29:17 +00:00
last_time_this_ran = self._timestamps[ 'last_check_idle_time' ]
2014-02-05 20:54:28 +00:00
2014-08-06 20:29:17 +00:00
self._timestamps[ 'last_check_idle_time' ] = HC.GetNow()
2014-02-05 20:54:28 +00:00
2014-08-06 20:29:17 +00:00
# this tests if we probably just woke up from a sleep
2014-08-13 22:18:12 +00:00
if HC.GetNow() - last_time_this_ran > MAINTENANCE_PERIOD + ( 5 * 60 ): return
2014-08-06 20:29:17 +00:00
if self.CurrentlyIdle(): self.MaintainDB()
2014-02-05 20:54:28 +00:00
2013-02-19 00:11:43 +00:00
def WaitUntilGoodTimeToUseGUIThread( self ):
while True:
if HC.shutdown: raise Exception( 'Client shutting down!' )
2014-06-18 21:53:48 +00:00
elif HC.pubsub.NoJobsQueued() and not HC.currently_doing_pubsub: return
2013-05-29 20:19:54 +00:00
else: time.sleep( 0.0001 )
2013-02-19 00:11:43 +00:00
def Write( self, action, *args, **kwargs ):
2013-12-04 22:44:16 +00:00
if action == 'content_updates': self._managers[ 'undo' ].AddCommand( 'content_updates', *args, **kwargs )
2013-07-10 20:25:57 +00:00
2013-07-31 21:26:38 +00:00
return self._Write( action, HC.HIGH_PRIORITY, False, *args, **kwargs )
2013-02-19 00:11:43 +00:00
2013-08-14 20:21:49 +00:00
def WriteSynchronous( self, action, *args, **kwargs ):
2013-07-31 21:26:38 +00:00
result = self._Write( action, HC.LOW_PRIORITY, True, *args, **kwargs )
time.sleep( 0.1 )
return result
2013-02-19 00:11:43 +00:00