hydrus/include/ClientController.py

1447 lines
47 KiB
Python
Executable File

import os
import wx
wx_first_num = int( wx.__version__[0] )
if wx_first_num < 4:
wx_error = 'Unfortunately, hydrus now requires the new Phoenix (4.x) version of wx.'
wx_error += os.linesep * 2
wx_error += 'Please check the \'running from source\' page in the html help for more details.'
raise Exception( wx_error )
import ClientCaches
import ClientData
import ClientDaemons
import ClientDefaults
import ClientGUICommon
import ClientGUIMenus
import ClientNetworking
import ClientNetworkingBandwidth
import ClientNetworkingDomain
import ClientNetworkingLogin
import ClientNetworkingSessions
import ClientOptions
import ClientPaths
import ClientThreading
import hashlib
import HydrusConstants as HC
import HydrusController
import HydrusData
import HydrusExceptions
import HydrusGlobals as HG
import HydrusNetworking
import HydrusPaths
import HydrusSerialisable
import HydrusThreading
import HydrusVideoHandling
import ClientConstants as CC
import ClientDB
import ClientGUI
import ClientGUIDialogs
import ClientGUIScrolledPanelsManagement
import ClientGUITopLevelWindows
import gc
import psutil
import threading
import time
import traceback
if not HG.twisted_is_broke:
from twisted.internet import reactor, defer
class Controller( HydrusController.HydrusController ):
def __init__( self, db_dir, no_daemons, no_wal ):
self._last_shutdown_was_bad = False
self._is_booted = False
self._splash = None
HydrusController.HydrusController.__init__( self, db_dir, no_daemons, no_wal )
self._name = 'client'
HG.client_controller = self
# just to set up some defaults, in case some db update expects something for an odd yaml-loading reason
self.options = ClientDefaults.GetClientDefaultOptions()
self.new_options = ClientOptions.ClientOptions( self.db_dir )
HC.options = self.options
self._page_key_lock = threading.Lock()
self._alive_page_keys = set()
self._closed_page_keys = set()
self._last_mouse_position = None
self._menu_open = False
self._previously_idle = False
self._idle_started = None
self.client_files_manager = None
self.services_manager = None
def _InitDB( self ):
return ClientDB.DB( self, self.db_dir, 'client', no_wal = self._no_wal )
def _InitTempDir( self ):
self.temp_dir = ClientPaths.GetTempDir()
def _DestroySplash( self ):
if self._splash is not None:
self._splash.DestroyLater()
self._splash = None
def _ReportShutdownDaemonsStatus( self ):
names = { daemon.name for daemon in self._daemons if daemon.is_alive() }
names = list( names )
names.sort()
self.pub( 'splash_set_status_subtext', ', '.join( names ) )
def AcquirePageKey( self ):
with self._page_key_lock:
page_key = HydrusData.GenerateKey()
self._alive_page_keys.add( page_key )
return page_key
def CallBlockingToWx( self, func, *args, **kwargs ):
def wx_code( job_key ):
try:
result = func( *args, **kwargs )
job_key.SetVariable( 'result', result )
except HydrusExceptions.PermissionException as e:
job_key.SetVariable( 'error', e )
except Exception as e:
job_key.SetVariable( 'error', e )
HydrusData.Print( 'CallBlockingToWx just caught this error:' )
HydrusData.DebugPrint( traceback.format_exc() )
finally:
job_key.Finish()
job_key = ClientThreading.JobKey()
job_key.Begin()
wx.CallAfter( wx_code, job_key )
while not job_key.IsDone():
if self._model_shutdown:
raise HydrusExceptions.ShutdownException( 'Application is shutting down!' )
time.sleep( 0.05 )
if job_key.HasVariable( 'result' ):
# result can be None, for wx_code that has no return variable
result = job_key.GetIfHasVariable( 'result' )
return result
error = job_key.GetIfHasVariable( 'error' )
if error is not None:
raise error
raise HydrusExceptions.ShutdownException()
def CallLaterWXSafe( self, window, initial_delay, func, *args, **kwargs ):
job_scheduler = self._GetAppropriateJobScheduler( initial_delay )
call = HydrusData.Call( func, *args, **kwargs )
job = ClientThreading.WXAwareJob( self, job_scheduler, window, initial_delay, call )
job_scheduler.AddJob( job )
return job
def CallRepeatingWXSafe( self, window, initial_delay, period, func, *args, **kwargs ):
job_scheduler = self._GetAppropriateJobScheduler( period )
call = HydrusData.Call( func, *args, **kwargs )
job = ClientThreading.WXAwareRepeatingJob( self, job_scheduler, window, initial_delay, period, call )
job_scheduler.AddJob( job )
return job
def CheckAlreadyRunning( self ):
while HydrusData.IsAlreadyRunning( self.db_dir, 'client' ):
self.pub( 'splash_set_status_text', 'client already running' )
def wx_code():
message = 'It looks like another instance of this client is already running, so this instance cannot start.'
message += os.linesep * 2
message += 'If the old instance is closing and does not quit for a _very_ long time, it is usually safe to force-close it from task manager.'
with ClientGUIDialogs.DialogYesNo( self._splash, message, 'The client is already running.', yes_label = 'wait a bit, then try again', no_label = 'forget it' ) as dlg:
if dlg.ShowModal() != wx.ID_YES:
raise HydrusExceptions.PermissionException()
self.CallBlockingToWx( wx_code )
for i in range( 10, 0, -1 ):
if not HydrusData.IsAlreadyRunning( self.db_dir, 'client' ):
break
self.pub( 'splash_set_status_text', 'waiting ' + str( i ) + ' seconds' )
time.sleep( 1 )
def CheckMouseIdle( self ):
mouse_position = wx.GetMousePosition()
if self._last_mouse_position is None:
self._last_mouse_position = mouse_position
elif mouse_position != self._last_mouse_position:
idle_before_position_update = self.CurrentlyIdle()
self._timestamps[ 'last_mouse_action' ] = HydrusData.GetNow()
self._last_mouse_position = mouse_position
idle_after_position_update = self.CurrentlyIdle()
move_knocked_us_out_of_idle = ( not idle_before_position_update ) and idle_after_position_update
if move_knocked_us_out_of_idle:
self.pub( 'set_status_bar_dirty' )
def ClosePageKeys( self, page_keys ):
with self._page_key_lock:
self._closed_page_keys.update( page_keys )
def CreateSplash( self ):
try:
self._splash = ClientGUI.FrameSplash( self )
except:
HydrusData.Print( 'There was an error trying to start the splash screen!' )
HydrusData.Print( traceback.format_exc() )
raise
def CurrentlyIdle( self ):
if HG.force_idle_mode:
self._idle_started = 0
return True
if not HydrusData.TimeHasPassed( self._timestamps[ 'boot' ] + 120 ):
return False
idle_normal = self.options[ 'idle_normal' ]
idle_period = self.options[ 'idle_period' ]
idle_mouse_period = self.options[ 'idle_mouse_period' ]
if idle_normal:
currently_idle = True
if idle_period is not None:
if not HydrusData.TimeHasPassed( self._timestamps[ 'last_user_action' ] + idle_period ):
currently_idle = False
if idle_mouse_period is not None:
if not HydrusData.TimeHasPassed( self._timestamps[ 'last_mouse_action' ] + idle_mouse_period ):
currently_idle = False
else:
currently_idle = False
turning_idle = currently_idle and not self._previously_idle
self._previously_idle = currently_idle
if turning_idle:
self._idle_started = HydrusData.GetNow()
self.pub( 'wake_daemons' )
if not currently_idle:
self._idle_started = None
return currently_idle
def CurrentlyVeryIdle( self ):
if self._idle_started is not None and HydrusData.TimeHasPassed( self._idle_started + 3600 ):
return True
return False
def DoIdleShutdownWork( self ):
stop_time = HydrusData.GetNow() + ( self.options[ 'idle_shutdown_max_minutes' ] * 60 )
self.MaintainDB( stop_time = stop_time )
if not self.options[ 'pause_repo_sync' ]:
services = self.services_manager.GetServices( HC.REPOSITORIES )
for service in services:
if HydrusData.TimeHasPassed( stop_time ):
return
service.SyncProcessUpdates( only_when_idle = False, stop_time = stop_time )
def Exit( self ):
if HG.emergency_exit:
self.ShutdownView()
self.ShutdownModel()
HydrusData.CleanRunningFile( self.db_dir, 'client' )
else:
try:
idle_shutdown_action = self.options[ 'idle_shutdown' ]
if idle_shutdown_action in ( CC.IDLE_ON_SHUTDOWN, CC.IDLE_ON_SHUTDOWN_ASK_FIRST ):
idle_shutdown_max_minutes = self.options[ 'idle_shutdown_max_minutes' ]
time_to_stop = HydrusData.GetNow() + ( idle_shutdown_max_minutes * 60 )
work_to_do = self.GetIdleShutdownWorkDue( time_to_stop )
if len( work_to_do ) > 0:
if idle_shutdown_action == CC.IDLE_ON_SHUTDOWN_ASK_FIRST:
text = 'Is now a good time for the client to do up to ' + HydrusData.ToHumanInt( idle_shutdown_max_minutes ) + ' minutes\' maintenance work? (Will auto-no in 15 seconds)'
text += os.linesep * 2
text += 'The outstanding jobs appear to be:'
text += os.linesep * 2
text += os.linesep.join( work_to_do )
with ClientGUIDialogs.DialogYesNo( self._splash, text, title = 'Maintenance is due' ) as dlg_yn:
job = self.CallLaterWXSafe( dlg_yn, 15, dlg_yn.EndModal, wx.ID_NO )
try:
if dlg_yn.ShowModal() == wx.ID_YES:
HG.do_idle_shutdown_work = True
finally:
job.Cancel()
else:
HG.do_idle_shutdown_work = True
self.CallToThreadLongRunning( self.THREADExitEverything )
except:
self._DestroySplash()
HydrusData.DebugPrint( traceback.format_exc() )
HG.emergency_exit = True
self.Exit()
def GetApp( self ):
return self._app
def GetBandwidthManager( self ):
raise NotImplementedError()
def GetClipboardText( self ):
if wx.TheClipboard.Open():
data = wx.TextDataObject()
wx.TheClipboard.GetData( data )
wx.TheClipboard.Close()
text = data.GetText()
text = HydrusData.ToUnicode( text )
return text
else:
raise Exception( 'I could not get permission to access the clipboard.' )
def GetCommandFromShortcut( self, shortcut_names, shortcut ):
return self._shortcuts_manager.GetCommand( shortcut_names, shortcut )
def GetGUI( self ):
return self.gui
def GetIdleShutdownWorkDue( self, time_to_stop ):
work_to_do = []
work_to_do.extend( self.Read( 'maintenance_due', time_to_stop ) )
services = self.services_manager.GetServices( HC.REPOSITORIES )
for service in services:
if service.CanDoIdleShutdownWork():
work_to_do.append( service.GetName() + ' repository processing' )
return work_to_do
def GetNewOptions( self ):
return self.new_options
def InitClientFilesManager( self ):
def wx_code( missing_locations ):
with ClientGUITopLevelWindows.DialogManage( None, 'repair file system' ) as dlg:
panel = ClientGUIScrolledPanelsManagement.RepairFileSystemPanel( dlg, missing_locations )
dlg.SetPanel( panel )
if dlg.ShowModal() == wx.ID_OK:
self.client_files_manager = ClientCaches.ClientFilesManager( self )
missing_locations = self.client_files_manager.GetMissing()
else:
raise HydrusExceptions.PermissionException( 'File system failed, user chose to quit.' )
return missing_locations
self.client_files_manager = ClientCaches.ClientFilesManager( self )
missing_locations = self.client_files_manager.GetMissing()
while len( missing_locations ) > 0:
missing_locations = self.CallBlockingToWx( wx_code, missing_locations )
def InitModel( self ):
self.pub( 'splash_set_title_text', u'booting db\u2026' )
HydrusController.HydrusController.InitModel( self )
self.pub( 'splash_set_status_text', u'initialising managers' )
self.pub( 'splash_set_status_subtext', u'services' )
self.services_manager = ClientCaches.ServicesManager( self )
self.pub( 'splash_set_status_subtext', u'options' )
self.options = self.Read( 'options' )
self.new_options = self.Read( 'serialisable', HydrusSerialisable.SERIALISABLE_TYPE_CLIENT_OPTIONS )
HC.options = self.options
if self.new_options.GetBoolean( 'use_system_ffmpeg' ):
if HydrusVideoHandling.FFMPEG_PATH.startswith( HC.BIN_DIR ):
HydrusVideoHandling.FFMPEG_PATH = os.path.basename( HydrusVideoHandling.FFMPEG_PATH )
self.pub( 'splash_set_status_subtext', u'client files' )
self.InitClientFilesManager()
#
self.pub( 'splash_set_status_subtext', u'network' )
self.parsing_cache = ClientCaches.ParsingCache()
bandwidth_manager = self.Read( 'serialisable', HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_BANDWIDTH_MANAGER )
if bandwidth_manager is None:
bandwidth_manager = ClientNetworkingBandwidth.NetworkBandwidthManager()
ClientDefaults.SetDefaultBandwidthManagerRules( bandwidth_manager )
bandwidth_manager._dirty = True
wx.MessageBox( 'Your bandwidth manager was missing on boot! I have recreated a new empty one with default rules. Please check that your hard drive and client are ok and let the hydrus dev know the details if there is a mystery.' )
session_manager = self.Read( 'serialisable', HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_SESSION_MANAGER )
if session_manager is None:
session_manager = ClientNetworkingSessions.NetworkSessionManager()
session_manager._dirty = True
wx.MessageBox( 'Your session manager was missing on boot! I have recreated a new empty one. Please check that your hard drive and client are ok and let the hydrus dev know the details if there is a mystery.' )
domain_manager = self.Read( 'serialisable', HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
if domain_manager is None:
domain_manager = ClientNetworkingDomain.NetworkDomainManager()
ClientDefaults.SetDefaultDomainManagerData( domain_manager )
domain_manager._dirty = True
wx.MessageBox( 'Your domain manager was missing on boot! I have recreated a new empty one. Please check that your hard drive and client are ok and let the hydrus dev know the details if there is a mystery.' )
domain_manager.Initialise()
login_manager = ClientNetworkingLogin.NetworkLoginManager()
self.network_engine = ClientNetworking.NetworkEngine( self, bandwidth_manager, session_manager, domain_manager, login_manager )
self.CallToThreadLongRunning( self.network_engine.MainLoop )
#
self._shortcuts_manager = ClientCaches.ShortcutsManager( self )
self.local_booru_manager = ClientCaches.LocalBooruCache( self )
self.pub( 'splash_set_status_subtext', u'tag censorship' )
self._managers[ 'tag_censorship' ] = ClientCaches.TagCensorshipManager( self )
self.pub( 'splash_set_status_subtext', u'tag siblings' )
self._managers[ 'tag_siblings' ] = ClientCaches.TagSiblingsManager( self )
self.pub( 'splash_set_status_subtext', u'tag parents' )
self._managers[ 'tag_parents' ] = ClientCaches.TagParentsManager( self )
self._managers[ 'undo' ] = ClientCaches.UndoManager( self )
def wx_code():
self._caches[ 'images' ] = ClientCaches.RenderedImageCache( self )
self._caches[ 'thumbnail' ] = ClientCaches.ThumbnailCache( self )
CC.GlobalBMPs.STATICInitialise()
self.pub( 'splash_set_status_subtext', u'image caches' )
self.CallBlockingToWx( wx_code )
self.sub( self, 'ToClipboard', 'clipboard' )
self.sub( self, 'RestartBooru', 'restart_booru' )
def InitView( self ):
if self.options[ 'password' ] is not None:
self.pub( 'splash_set_status_text', 'waiting for password' )
def wx_code_password():
while True:
with wx.PasswordEntryDialog( self._splash, 'Enter your password', 'Enter password' ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
# this can produce unicode with cyrillic or w/e keyboards, which hashlib can't handle
password = HydrusData.ToByteString( dlg.GetValue() )
if hashlib.sha256( password ).digest() == self.options[ 'password' ]: break
else:
raise HydrusExceptions.PermissionException( 'Bad password check' )
self.CallBlockingToWx( wx_code_password )
self.pub( 'splash_set_title_text', u'booting gui\u2026' )
def wx_code_gui():
self.gui = ClientGUI.FrameGUI( self )
self.ResetIdleTimer()
self.CallBlockingToWx( wx_code_gui )
# ShowText will now popup as a message, as popup message manager has overwritten the hooks
HydrusController.HydrusController.InitView( self )
self._booru_port_connection = None
self.RestartBooru()
if not self._no_daemons:
self._daemons.append( HydrusThreading.DAEMONWorker( self, 'SynchroniseAccounts', ClientDaemons.DAEMONSynchroniseAccounts, ( 'notify_unknown_accounts', ) ) )
self._daemons.append( HydrusThreading.DAEMONWorker( self, 'SaveDirtyObjects', ClientDaemons.DAEMONSaveDirtyObjects, ( 'important_dirt_to_clean', ), period = 30 ) )
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'DownloadFiles', ClientDaemons.DAEMONDownloadFiles, ( 'notify_new_downloads', 'notify_new_permissions' ) ) )
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'SynchroniseSubscriptions', ClientDaemons.DAEMONSynchroniseSubscriptions, ( 'notify_restart_subs_sync_daemon', 'notify_new_subscriptions' ), period = 14400, init_wait = 60, pre_call_wait = 3 ) )
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'CheckImportFolders', ClientDaemons.DAEMONCheckImportFolders, ( 'notify_restart_import_folders_daemon', 'notify_new_import_folders' ), period = 180 ) )
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'CheckExportFolders', ClientDaemons.DAEMONCheckExportFolders, ( 'notify_restart_export_folders_daemon', 'notify_new_export_folders' ), period = 180 ) )
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'MaintainTrash', ClientDaemons.DAEMONMaintainTrash, init_wait = 120 ) )
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'SynchroniseRepositories', ClientDaemons.DAEMONSynchroniseRepositories, ( 'notify_restart_repo_sync_daemon', 'notify_new_permissions' ), period = 4 * 3600, pre_call_wait = 1 ) )
self._daemons.append( HydrusThreading.DAEMONBackgroundWorker( self, 'UPnP', ClientDaemons.DAEMONUPnP, ( 'notify_new_upnp_mappings', ), init_wait = 120, pre_call_wait = 6 ) )
self.CallRepeatingWXSafe( self, 10.0, 10.0, self.CheckMouseIdle )
if self.db.IsFirstStart():
message = 'Hi, this looks like the first time you have started the hydrus client.'
message += os.linesep * 2
message += 'Don\'t forget to check out the help if you haven\'t already--it has an extensive \'getting started\' section.'
message += os.linesep * 2
message += 'To dismiss popup messages like this, right-click them.'
HydrusData.ShowText( message )
if self.db.IsDBUpdated():
HydrusData.ShowText( 'The client has updated to version ' + str( HC.SOFTWARE_VERSION ) + '!' )
for message in self.db.GetInitialMessages():
HydrusData.ShowText( message )
def IsBooted( self ):
return self._is_booted
def LastShutdownWasBad( self ):
return self._last_shutdown_was_bad
def MaintainDB( self, stop_time = None ):
if self.new_options.GetBoolean( 'maintain_similar_files_duplicate_pairs_during_idle' ):
phashes_stop_time = stop_time
if phashes_stop_time is None:
phashes_stop_time = HydrusData.GetNow() + 15
self.WriteInterruptable( 'maintain_similar_files_phashes', stop_time = phashes_stop_time )
tree_stop_time = stop_time
if tree_stop_time is None:
tree_stop_time = HydrusData.GetNow() + 30
self.WriteInterruptable( 'maintain_similar_files_tree', stop_time = tree_stop_time, abandon_if_other_work_to_do = True )
search_distance = self.new_options.GetInteger( 'similar_files_duplicate_pairs_search_distance' )
search_stop_time = stop_time
if search_stop_time is None:
search_stop_time = HydrusData.GetNow() + 60
self.WriteInterruptable( 'maintain_similar_files_duplicate_pairs', search_distance, stop_time = search_stop_time, abandon_if_other_work_to_do = True )
if stop_time is None or not HydrusData.TimeHasPassed( stop_time ):
self.WriteInterruptable( 'maintain_file_reparsing', stop_time = stop_time )
if stop_time is None or not HydrusData.TimeHasPassed( stop_time ):
self.WriteInterruptable( 'vacuum', stop_time = stop_time )
if stop_time is None or not HydrusData.TimeHasPassed( stop_time ):
self.WriteInterruptable( 'analyze', stop_time = stop_time )
if stop_time is None or not HydrusData.TimeHasPassed( stop_time ):
if HydrusData.TimeHasPassed( self._timestamps[ 'last_service_info_cache_fatten' ] + ( 60 * 20 ) ):
self.pub( 'splash_set_status_text', 'fattening service info' )
services = self.services_manager.GetServices()
for service in services:
self.pub( 'splash_set_status_subtext', service.GetName() )
try: self.Read( 'service_info', service.GetServiceKey() )
except: pass # sometimes this breaks when a service has just been removed and the client is closing, so ignore the error
self._timestamps[ 'last_service_info_cache_fatten' ] = HydrusData.GetNow()
def MaintainMemoryFast( self ):
HydrusController.HydrusController.MaintainMemoryFast( self )
self.parsing_cache.CleanCache()
def MaintainMemorySlow( self ):
HydrusController.HydrusController.MaintainMemorySlow( self )
if HydrusData.TimeHasPassed( self._timestamps[ 'last_page_change' ] + 30 * 60 ):
self.pub( 'delete_old_closed_pages' )
self._timestamps[ 'last_page_change' ] = HydrusData.GetNow()
disk_cache_maintenance_mb = self.new_options.GetNoneableInteger( 'disk_cache_maintenance_mb' )
if disk_cache_maintenance_mb is not None:
if self.CurrentlyVeryIdle():
cache_period = 3600
disk_cache_stop_time = HydrusData.GetNow() + 30
elif self.CurrentlyIdle():
cache_period = 1800
disk_cache_stop_time = HydrusData.GetNow() + 10
else:
cache_period = 240
disk_cache_stop_time = HydrusData.GetNow() + 2
if HydrusData.TimeHasPassed( self._timestamps[ 'last_disk_cache_population' ] + cache_period ):
self.Read( 'load_into_disk_cache', stop_time = disk_cache_stop_time, caller_limit = disk_cache_maintenance_mb * 1024 * 1024 )
self._timestamps[ 'last_disk_cache_population' ] = HydrusData.GetNow()
def MenuIsOpen( self ):
return self._menu_open
def PageAlive( self, page_key ):
with self._page_key_lock:
return page_key in self._alive_page_keys
def PageClosedButNotDestroyed( self, page_key ):
with self._page_key_lock:
return page_key in self._closed_page_keys
def PopupMenu( self, window, menu ):
if menu.GetMenuItemCount() > 0:
self._menu_open = True
window.PopupMenu( menu )
self._menu_open = False
ClientGUIMenus.DestroyMenu( window, menu )
def PrepStringForDisplay( self, text ):
return text.lower()
def ProcessPubSub( self ):
self.CallBlockingToWx( self._pubsub.Process )
def RefreshServices( self ):
self.services_manager.RefreshServices()
def ReleasePageKey( self, page_key ):
with self._page_key_lock:
self._alive_page_keys.discard( page_key )
self._closed_page_keys.discard( page_key )
def ResetPageChangeTimer( self ):
self._timestamps[ 'last_page_change' ] = HydrusData.GetNow()
def RestartBooru( self ):
service = self.services_manager.GetService( CC.LOCAL_BOORU_SERVICE_KEY )
port = service.GetPort()
def TWISTEDRestartServer():
def StartServer( *args, **kwargs ):
try:
try:
connection = HydrusNetworking.GetLocalConnection( port )
connection.close()
text = 'The client\'s booru server could not start because something was already bound to port ' + str( 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.'
HydrusData.ShowText( text )
except:
import ClientLocalServer
self._booru_port_connection = reactor.listenTCP( port, ClientLocalServer.HydrusServiceBooru( service ) )
try:
connection = HydrusNetworking.GetLocalConnection( port )
connection.close()
except Exception as e:
text = 'Tried to bind port ' + str( port ) + ' for the local booru, but it failed:'
text += os.linesep * 2
text += HydrusData.ToUnicode( e )
HydrusData.ShowText( text )
except Exception as e:
wx.CallAfter( HydrusData.ShowException, e )
if self._booru_port_connection is None:
if port is not None:
StartServer()
else:
deferred = defer.maybeDeferred( self._booru_port_connection.stopListening )
if port is not None:
deferred.addCallback( StartServer )
if HG.twisted_is_broke:
HydrusData.ShowText( 'Twisted failed to import, so could not restart the booru! Please contact hydrus dev!' )
else:
reactor.callFromThread( TWISTEDRestartServer )
def RestoreDatabase( self ):
restore_intro = ''
with wx.DirDialog( self.gui, 'Select backup location.' ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
path = HydrusData.ToUnicode( dlg.GetPath() )
text = 'Are you sure you want to restore a backup from "' + path + '"?'
text += os.linesep * 2
text += 'Everything in your current database will be deleted!'
text += os.linesep * 2
text += '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, text ) as dlg_yn:
if dlg_yn.ShowModal() == wx.ID_YES:
def THREADRestart():
wx.CallAfter( self.gui.Exit )
while not self.db.LoopIsFinished():
time.sleep( 0.1 )
self.db.RestoreBackup( path )
while not HG.shutdown_complete:
time.sleep( 0.1 )
HydrusData.RestartProcess()
self.CallToThreadLongRunning( THREADRestart )
def Run( self ):
self._app = wx.App()
self._app.locale = wx.Locale( wx.LANGUAGE_DEFAULT ) # Very important to init this here and keep it non garbage collected
# do not import locale here and try anything clever--assume that bad locale formatting is due to OS-level mess-up, not mine
# wx locale is supposed to set it all up nice, so if someone's doesn't, explore that and find the external solution
HydrusData.Print( u'booting controller\u2026' )
self.frame_icon = wx.Icon( os.path.join( HC.STATIC_DIR, 'hydrus_32_non-transparent.png' ), wx.BITMAP_TYPE_PNG )
self.CreateSplash()
self.CallToThreadLongRunning( self.THREADBootEverything )
self._app.MainLoop()
HydrusData.Print( u'shutting down controller\u2026' )
def SaveDirtyObjects( self ):
with HG.dirty_object_lock:
dirty_services = [ service for service in self.services_manager.GetServices() if service.IsDirty() ]
if len( dirty_services ) > 0:
self.WriteSynchronous( 'dirty_services', dirty_services )
if self.network_engine.bandwidth_manager.IsDirty():
self.WriteSynchronous( 'serialisable', self.network_engine.bandwidth_manager )
self.network_engine.bandwidth_manager.SetClean()
if self.network_engine.domain_manager.IsDirty():
self.WriteSynchronous( 'serialisable', self.network_engine.domain_manager )
self.network_engine.domain_manager.SetClean()
if self.network_engine.session_manager.IsDirty():
self.WriteSynchronous( 'serialisable', self.network_engine.session_manager )
self.network_engine.session_manager.SetClean()
def SetServices( self, services ):
with HG.dirty_object_lock:
self.WriteSynchronous( 'update_services', services )
self.services_manager.RefreshServices()
def ShutdownModel( self ):
if not HG.emergency_exit:
self.SaveDirtyObjects()
HydrusController.HydrusController.ShutdownModel( self )
def ShutdownView( self ):
if not HG.emergency_exit:
self.pub( 'splash_set_status_text', 'waiting for daemons to exit' )
self._ShutdownDaemons()
if HG.do_idle_shutdown_work:
try:
self.DoIdleShutdownWork()
except:
ClientData.ReportShutdownException()
HydrusController.HydrusController.ShutdownView( self )
def SystemBusy( self ):
if HG.force_idle_mode:
return False
max_cpu = self.options[ 'idle_cpu_max' ]
if max_cpu is None:
self._system_busy = False
else:
if HydrusData.TimeHasPassed( self._timestamps[ 'last_cpu_check' ] + 60 ):
cpu_times = psutil.cpu_percent( percpu = True )
if True in ( cpu_time > max_cpu for cpu_time in cpu_times ):
self._system_busy = True
else:
self._system_busy = False
self._timestamps[ 'last_cpu_check' ] = HydrusData.GetNow()
return self._system_busy
def THREADBootEverything( self ):
try:
self.CheckAlreadyRunning()
self._last_shutdown_was_bad = HydrusData.LastShutdownWasBad( self.db_dir, 'client' )
HydrusData.RecordRunningStart( self.db_dir, 'client' )
self.InitModel()
self.InitView()
self._is_booted = True
except HydrusExceptions.PermissionException as e:
HydrusData.Print( e )
HG.emergency_exit = True
self.Exit()
except Exception as e:
text = 'A serious error occurred while trying to start the program. The error will be shown next in a window. More information may have been written to client.log.'
HydrusData.DebugPrint( 'If the db crashed, another error may be written just above ^.' )
HydrusData.DebugPrint( text )
HydrusData.DebugPrint( traceback.format_exc() )
wx.CallAfter( wx.MessageBox, traceback.format_exc() )
wx.CallAfter( wx.MessageBox, text )
HG.emergency_exit = True
self.Exit()
finally:
self._DestroySplash()
def THREADExitEverything( self ):
try:
self.pub( 'splash_set_title_text', u'shutting down gui\u2026' )
self.ShutdownView()
self.pub( 'splash_set_title_text', u'shutting down db\u2026' )
self.ShutdownModel()
self.pub( 'splash_set_title_text', u'cleaning up\u2026' )
self.pub( 'splash_set_status_text', u'' )
HydrusData.CleanRunningFile( self.db_dir, 'client' )
except HydrusExceptions.PermissionException:
pass
except HydrusExceptions.ShutdownException:
pass
except:
ClientData.ReportShutdownException()
finally:
self._DestroySplash()
def ToClipboard( self, data_type, data ):
# need this cause can't do it in a non-gui thread
if data_type == 'paths':
paths = data
if wx.TheClipboard.Open():
data = wx.DataObjectComposite()
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 )
wx.TheClipboard.SetData( data )
wx.TheClipboard.Close()
else: wx.MessageBox( 'Could not get permission to access the clipboard!' )
elif data_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.' )
elif data_type == 'bmp':
media = data
image_renderer = self.GetCache( 'images' ).GetImageRenderer( media )
def CopyToClipboard():
if wx.TheClipboard.Open():
wx_bmp = image_renderer.GetWXBitmap()
data = wx.BitmapDataObject( wx_bmp )
wx.TheClipboard.SetData( data )
wx.TheClipboard.Close()
else:
wx.MessageBox( 'I could not get permission to access the clipboard.' )
def THREADWait():
# have to do this in thread, because the image needs the wx event queue to render
start_time = time.time()
while not image_renderer.IsReady():
if HydrusData.TimeHasPassed( start_time + 15 ):
raise Exception( 'The image did not render in fifteen seconds, so the attempt to copy it to the clipboard was abandoned.' )
time.sleep( 0.1 )
wx.CallAfter( CopyToClipboard )
self.CallToThread( THREADWait )
def UnclosePageKeys( self, page_keys ):
with self._page_key_lock:
self._closed_page_keys.difference_update( page_keys )
def WaitUntilViewFree( self ):
self.WaitUntilModelFree()
self.WaitUntilThumbnailsFree()
def WaitUntilThumbnailsFree( self ):
while True:
if self._view_shutdown:
raise HydrusExceptions.ShutdownException( 'Application shutting down!' )
elif not self._caches[ 'thumbnail' ].DoingWork():
return
else:
time.sleep( 0.00001 )
def Write( self, action, *args, **kwargs ):
if action == 'content_updates':
self._managers[ 'undo' ].AddCommand( 'content_updates', *args, **kwargs )
return HydrusController.HydrusController.Write( self, action, *args, **kwargs )