2015-10-12 00:23:37 +00:00
#!/usr/bin/env python3
2013-04-27 22:23:19 +00:00
2017-03-20 13:06:01 +00:00
# Copyright (C) 2013-2017 Christian Thomas Jacobs.
2013-04-27 22:23:19 +00:00
# This file is part of PyQSO.
# PyQSO is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyQSO is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyQSO. If not, see <http://www.gnu.org/licenses/>.
2017-02-07 17:22:03 +00:00
from gi . repository import Gtk , GObject , Gdk
2013-04-27 22:23:19 +00:00
import logging
import telnetlib
2017-02-07 14:17:14 +00:00
try :
import configparser
except ImportError :
import ConfigParser as configparser
2015-11-28 16:41:23 +00:00
import os . path
2013-04-27 22:23:19 +00:00
2017-05-09 16:26:25 +00:00
from pyqso . telnet_connection_dialog import TelnetConnectionDialog
from pyqso . auxiliary_dialogs import error
2015-11-29 22:25:41 +00:00
BOOKMARKS_FILE = os . path . expanduser ( ' ~/.config/pyqso/bookmarks.ini ' )
2015-11-28 16:55:28 +00:00
2016-01-27 16:23:09 +00:00
2017-02-21 14:54:04 +00:00
class DXCluster :
2016-01-27 16:23:09 +00:00
""" A tool for connecting to a DX cluster (specifically Telnet-based DX clusters). """
2017-04-01 17:10:24 +00:00
def __init__ ( self , application ) :
2017-04-01 16:55:03 +00:00
""" Set up the DX cluster, and set up a timer so that PyQSO can retrieve new data from the Telnet server every few seconds.
2016-01-27 16:23:09 +00:00
2017-04-01 17:10:24 +00:00
: arg application : The PyQSO application containing the main Gtk window , etc .
2016-01-27 16:23:09 +00:00
"""
2017-02-21 14:54:04 +00:00
2016-01-27 16:23:09 +00:00
logging . debug ( " Setting up the DX cluster... " )
2017-04-01 17:10:24 +00:00
self . application = application
self . builder = self . application . builder
2016-01-27 16:23:09 +00:00
self . connection = None
2017-02-21 14:54:04 +00:00
# Connect signals.
self . builder . get_object ( " mitem_new " ) . connect ( " activate " , self . new_server )
self . builder . get_object ( " mitem_disconnect " ) . connect ( " activate " , self . telnet_disconnect )
self . builder . get_object ( " send " ) . connect ( " clicked " , self . telnet_send_command )
2017-06-27 21:04:32 +00:00
self . builder . get_object ( " command " ) . connect ( " key-press-event " , self . on_command_key_press )
2016-01-27 16:23:09 +00:00
2017-02-21 14:54:04 +00:00
# Get the text renderer and its buffer.
self . renderer = self . builder . get_object ( " renderer " )
2016-01-27 16:23:09 +00:00
self . buffer = self . renderer . get_buffer ( )
2017-02-21 14:54:04 +00:00
# Items whose sensitivity may change.
self . items = { }
self . items [ " CONNECT " ] = self . builder . get_object ( " mitem_connect " )
self . items [ " DISCONNECT " ] = self . builder . get_object ( " mitem_disconnect " )
self . items [ " SEND " ] = self . builder . get_object ( " send " )
2016-01-27 16:23:09 +00:00
self . set_items_sensitive ( True )
2017-04-14 20:45:59 +00:00
self . populate_bookmarks ( )
2016-01-27 16:23:09 +00:00
logging . debug ( " DX cluster ready! " )
return
2017-04-14 20:45:59 +00:00
def on_command_key_press ( self , widget , event , data = None ) :
2017-02-07 17:22:03 +00:00
""" If the Return key is pressed when the focus is on the command box, then send whatever command the user has entered. """
2017-03-02 10:45:49 +00:00
if ( event . keyval == Gdk . KEY_Return ) :
2017-02-07 17:22:03 +00:00
self . telnet_send_command ( )
return
2016-01-27 16:23:09 +00:00
def new_server ( self , widget = None ) :
2017-02-15 18:04:49 +00:00
""" Get Telnet server host and login details specified in the Gtk.Entry boxes in the Telnet connection dialog and attempt a connection. """
2017-05-09 16:26:25 +00:00
# Get connection details.
tcd = TelnetConnectionDialog ( self . application )
response = tcd . dialog . run ( )
2016-01-27 16:23:09 +00:00
if ( response == Gtk . ResponseType . OK ) :
2017-05-09 16:26:25 +00:00
host = tcd . host
port = tcd . port
username = tcd . username
password = tcd . password
bookmark = tcd . bookmark
tcd . dialog . destroy ( )
2016-01-27 16:23:09 +00:00
2017-02-08 16:36:56 +00:00
# Handle empty hostname.
2017-05-09 16:26:25 +00:00
if ( not host ) :
2017-02-08 16:36:56 +00:00
logging . error ( " No hostname specified. " )
return
# Handle empty port number.
2017-05-09 16:26:25 +00:00
if ( not port ) :
2017-02-08 16:36:56 +00:00
logging . warning ( " No port specified. Assuming default port 23... " )
port = 23
else :
try :
# Cast port into an int.
port = int ( port )
except ValueError as e :
logging . error ( " Could not cast the DX cluster ' s port information to an integer. " )
logging . exception ( e )
return
2016-01-27 16:23:09 +00:00
# Save the server details in a new bookmark, if desired.
2017-05-09 16:26:25 +00:00
if ( bookmark ) :
2016-01-27 16:23:09 +00:00
try :
config = configparser . ConfigParser ( )
config . read ( BOOKMARKS_FILE )
# Use the host name as the bookmark's identifier.
2017-05-09 16:26:25 +00:00
if ( username ) :
2017-02-08 16:36:56 +00:00
bookmark_identifier = " %s @ %s : %d " % ( username , host , port )
else :
bookmark_identifier = " %s : %d " % ( host , port )
logging . debug ( " Using %s as the bookmark identifier. " % bookmark_identifier )
# Add bookmark.
2016-01-27 16:23:09 +00:00
try :
2017-02-08 16:36:56 +00:00
config . add_section ( bookmark_identifier )
2016-01-27 16:23:09 +00:00
except configparser . DuplicateSectionError :
# If the hostname already exists, assume the user wants to update the port number, username and/or password.
2017-02-08 16:36:56 +00:00
logging . warning ( " Bookmark ' %s ' already exists. Over-writing existing details... " % ( bookmark_identifier ) )
config . set ( bookmark_identifier , " host " , host )
config . set ( bookmark_identifier , " port " , str ( port ) )
config . set ( bookmark_identifier , " username " , username )
config . set ( bookmark_identifier , " password " , password )
2016-01-27 16:23:09 +00:00
# Write the bookmarks to file.
if not os . path . exists ( os . path . expanduser ( ' ~/.config/pyqso ' ) ) :
os . makedirs ( os . path . expanduser ( ' ~/.config/pyqso ' ) )
with open ( BOOKMARKS_FILE , ' w ' ) as f :
config . write ( f )
2017-04-14 20:45:59 +00:00
self . populate_bookmarks ( )
2016-01-27 16:23:09 +00:00
except IOError :
# Maybe the bookmarks file could not be written to?
logging . error ( " Bookmark could not be saved. Check bookmarks file permissions? Going ahead with the server connection anyway... " )
2017-02-08 16:36:56 +00:00
# Attempt a connection with the server.
self . telnet_connect ( host , port , username , password )
2016-01-27 16:23:09 +00:00
else :
2017-05-09 16:26:25 +00:00
tcd . dialog . destroy ( )
2016-01-27 16:23:09 +00:00
return
2017-04-14 20:45:59 +00:00
def populate_bookmarks ( self ) :
2016-01-27 16:23:09 +00:00
""" Populate the list of bookmarked Telnet servers in the menu. """
2017-02-21 14:54:04 +00:00
# Get the bookmarks submenu.
subm_bookmarks = self . builder . get_object ( " subm_bookmarks " )
2016-01-27 16:23:09 +00:00
config = configparser . ConfigParser ( )
have_config = ( config . read ( BOOKMARKS_FILE ) != [ ] )
if ( have_config ) :
2015-11-28 16:41:23 +00:00
try :
2016-01-27 16:23:09 +00:00
# Clear the menu of all current bookmarks.
2017-02-21 14:54:04 +00:00
for i in subm_bookmarks . get_children ( ) :
subm_bookmarks . remove ( i )
2016-01-27 16:23:09 +00:00
# Add all bookmarks in the config file.
for bookmark in config . sections ( ) :
mitem = Gtk . MenuItem ( label = bookmark )
mitem . connect ( " activate " , self . bookmarked_server , bookmark )
2017-02-21 14:54:04 +00:00
subm_bookmarks . append ( mitem )
2016-01-27 16:23:09 +00:00
except Exception as e :
logging . error ( " An error occurred whilst populating the DX cluster bookmarks menu. " )
logging . exception ( e )
2017-02-21 14:54:04 +00:00
self . builder . get_object ( " dx_cluster " ) . show_all ( ) # Need to do this to update the bookmarks list in the menu.
2016-01-27 16:23:09 +00:00
return
def bookmarked_server ( self , widget , name ) :
""" Get Telnet server host and login details from an existing bookmark and attempt a connection.
: arg str name : The name of the bookmark . This is the same as the server ' s hostname.
"""
config = configparser . ConfigParser ( )
have_config = ( config . read ( BOOKMARKS_FILE ) != [ ] )
try :
if ( not have_config ) :
raise IOError ( " The bookmark ' s details could not be loaded. " )
host = config . get ( name , " host " )
port = int ( config . get ( name , " port " ) )
username = config . get ( name , " username " )
password = config . get ( name , " password " )
2015-11-28 16:41:23 +00:00
self . telnet_connect ( host , port , username , password )
2016-01-27 16:23:09 +00:00
except ValueError as e :
# This exception may occur when casting the port (which is a str) to an int.
logging . exception ( e )
except IOError as e :
logging . exception ( e )
except Exception as e :
2017-02-08 16:36:56 +00:00
logging . error ( " Could not connect to Telnet server ' %s ' . " % name )
2015-11-28 16:41:23 +00:00
logging . exception ( e )
2016-01-27 16:23:09 +00:00
return
def telnet_connect ( self , host , port = 23 , username = None , password = None ) :
""" Connect to a user-specified Telnet server.
: arg str host : The Telnet server ' s hostname.
: arg int port : The Telnet server ' s port number. If no port is specified, the default Telnet server port of 23 will be used.
: arg str username : The user ' s username. This is an optional argument.
: arg str password : The user ' s password. This is an optional argument.
"""
2017-05-09 16:26:25 +00:00
# Handle empty host/port string (or the case where host/port are None).
if ( not host ) :
2017-05-17 16:36:33 +00:00
error ( parent = self . application . window , message = " Unable to connect to a DX cluster because no hostname was specified. " )
2016-01-27 16:23:09 +00:00
return
2017-05-09 16:26:25 +00:00
if ( not port ) :
2017-02-08 16:36:56 +00:00
logging . warning ( " No port specified. Assuming default port 23... " )
2017-05-09 16:26:25 +00:00
port = 23 # Use the default Telnet port.
2016-01-27 16:23:09 +00:00
try :
2017-05-09 16:26:25 +00:00
logging . debug ( " Attempting connection to Telnet server %s : %d ... " % ( host , port ) )
2016-01-27 16:23:09 +00:00
self . connection = telnetlib . Telnet ( host , port )
2017-05-09 16:26:25 +00:00
assert ( self . connection )
2016-01-27 16:23:09 +00:00
if ( username ) :
self . connection . read_until ( " login: " . encode ( ) )
self . connection . write ( ( username + " \n " ) . encode ( ) )
if ( password ) :
self . connection . read_until ( " password: " . encode ( ) )
self . connection . write ( ( password + " \n " ) . encode ( ) )
except Exception as e :
2017-05-09 16:26:25 +00:00
message = " Could not create a connection to the Telnet server %s : %d . Check connection to the internets? Check connection details? " % ( host , port )
error ( parent = self . application . window , message = message )
2017-05-17 16:36:33 +00:00
logging . exception ( e )
2016-01-27 16:23:09 +00:00
self . connection = None
return
2017-05-09 16:26:25 +00:00
logging . debug ( " Connection to %s : %d established. " % ( host , port ) )
2016-01-27 16:23:09 +00:00
self . set_items_sensitive ( False )
2015-11-28 16:41:23 +00:00
2017-04-14 20:45:59 +00:00
self . check_io_event = GObject . timeout_add ( 1000 , self . on_telnet_io )
2016-01-27 16:23:09 +00:00
return
def telnet_disconnect ( self , widget = None ) :
""" Disconnect from a Telnet server and remove the I/O timer. """
if ( self . connection ) :
self . connection . close ( )
self . buffer . set_text ( " " )
self . connection = None
self . set_items_sensitive ( True )
# Stop checking for server output once disconnected.
try :
GObject . source_remove ( self . check_io_event )
except AttributeError :
# This may happen if a connection hasn't yet been established.
2015-02-28 16:19:52 +00:00
pass
2016-01-27 16:23:09 +00:00
return
def telnet_send_command ( self , widget = None ) :
""" Send the user-specified command in the Gtk.Entry box to the Telnet server (if PyQSO is connected to one). """
if ( self . connection ) :
2017-02-21 14:54:04 +00:00
command = self . builder . get_object ( " command " )
self . connection . write ( ( command . get_text ( ) + " \n " ) . encode ( ) )
command . set_text ( " " )
2016-01-27 16:23:09 +00:00
return
2017-04-14 20:45:59 +00:00
def on_telnet_io ( self ) :
2016-01-27 16:23:09 +00:00
""" Retrieve any new data from the Telnet server and print it out in the Gtk.TextView widget.
: returns : Always returns True to satisfy the GObject timer .
: rtype : bool
"""
if ( self . connection ) :
2017-02-07 20:33:14 +00:00
text = self . connection . read_very_eager ( )
text = text . decode ( " ascii " , " replace " ) # Replace any characters that cannot be decoded with a replacement marker.
2016-01-27 16:23:09 +00:00
try :
text = text . replace ( " \u0007 " , " " ) # Remove the BEL Unicode character from the end of the line
except UnicodeDecodeError :
pass
# Allow auto-scrolling to the new text entry if the focus is already at
# the very end of the Gtk.TextView. Otherwise, don't auto-scroll
# in case the user is reading something further up.
# Note: This is based on the code from http://forums.gentoo.org/viewtopic-t-445598-view-next.html
end_iter = self . buffer . get_end_iter ( )
2013-06-07 23:54:53 +00:00
end_mark = self . buffer . create_mark ( None , end_iter )
2016-01-27 16:23:09 +00:00
self . renderer . move_mark_onscreen ( end_mark )
at_end = self . buffer . get_iter_at_mark ( end_mark ) . equal ( end_iter )
self . buffer . insert ( end_iter , text )
if ( at_end ) :
end_mark = self . buffer . create_mark ( None , end_iter )
self . renderer . scroll_mark_onscreen ( end_mark )
2013-06-07 23:54:53 +00:00
2016-01-27 16:23:09 +00:00
return True
def set_items_sensitive ( self , sensitive ) :
""" Enable/disable the relevant buttons for connecting/disconnecting from a DX cluster, so that users cannot click the connect button if PyQSO is already connected.
: arg bool sensitive : If True , enable the Connect button and disable the Disconnect button . If False , vice versa .
"""
self . items [ " CONNECT " ] . set_sensitive ( sensitive )
self . items [ " DISCONNECT " ] . set_sensitive ( not sensitive )
2017-02-21 14:54:04 +00:00
self . items [ " SEND " ] . set_sensitive ( not sensitive )
2016-01-27 16:23:09 +00:00
return