From 2808221e0cc4dd00009ab84983f98602ac736980 Mon Sep 17 00:00:00 2001 From: Seth Hendrick Date: Sun, 20 Jan 2019 16:45:35 -0500 Subject: [PATCH] Issue: #29 Added IrcMac, which is an abstraction layer for writing/reading from the TcpClient. This will prevent NREs from happening since we never set this to null. Our ReceiverThread will also return for ANY Exception, instead of swallowing it and causing an NRE (or now, an ODE) from happening over and over again.. --- Chaskis/ChaskisCore/IrcConnection.cs | 106 ++++++---------- Chaskis/ChaskisCore/IrcMac.cs | 182 +++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 68 deletions(-) create mode 100644 Chaskis/ChaskisCore/IrcMac.cs diff --git a/Chaskis/ChaskisCore/IrcConnection.cs b/Chaskis/ChaskisCore/IrcConnection.cs index 9b952213..c99bbf35 100644 --- a/Chaskis/ChaskisCore/IrcConnection.cs +++ b/Chaskis/ChaskisCore/IrcConnection.cs @@ -1,5 +1,5 @@ // -// Copyright Seth Hendrick 2016-2018. +// Copyright Seth Hendrick 2016-2019. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net.Security; using System.Net.Sockets; using System.Threading; using SethCS.Basic; @@ -58,28 +57,13 @@ public class IrcConnection : IDisposable, IConnection, IChaskisEventScheduler, I /// public static readonly int MaximumLength = 400; - /// - /// Connection to the server. - /// - private TcpClient connection; - - private SslStream sslStream; - - /// - /// Used to send commands. - /// - private StreamWriter ircWriter; + private readonly IIrcMac connection; /// /// Lock for the IRC writer. /// private readonly object ircWriterLock; - /// - /// Used to read commands. - /// - private StreamReader ircReader; - /// /// The thread that reads. /// @@ -122,16 +106,20 @@ public class IrcConnection : IDisposable, IConnection, IChaskisEventScheduler, I /// Constructor /// /// The configuration to use. - public IrcConnection( IIrcConfig config, INonDisposableStringParsingQueue parsingQueue ) + public IrcConnection( IIrcConfig config, INonDisposableStringParsingQueue parsingQueue, IIrcMac macLayer = null ) { this.inited = false; this.Config = new ReadOnlyIrcConfig( config ); this.IsConnected = false; - this.connection = null; - this.sslStream = null; - this.ircWriter = null; - this.ircReader = null; + if( macLayer == null ) + { + this.connection = new IrcMac( config, StaticLogger.Log ); + } + else + { + this.connection = macLayer; + } this.keepReadingObject = new object(); this.KeepReading = false; @@ -227,30 +215,22 @@ public void Connect() ); } - // Connect. - this.connection = new TcpClient( this.Config.Server, this.Config.Port ); - - Stream stream; - if( this.Config.UseSsl ) + if( this.readerThread != null ) { - StaticLogger.Log.WriteLine( "Using SSL connection." ); - this.sslStream = new SslStream( this.connection.GetStream() ); - this.sslStream.AuthenticateAsClient( this.Config.Server ); - stream = sslStream; - } - else - { - StaticLogger.Log.WriteLine( "WARNING! Using plain text connection." ); - stream = this.connection.GetStream(); + throw new InvalidOperationException( + "Somehow our reader thread is not null, this should never happen. Something went wrong." + ); } - this.ircWriter = new StreamWriter( stream ); - this.ircReader = new StreamReader( stream ); + // Connect. + this.connection.Connect(); // Start Reading. this.KeepReading = true; - this.readerThread = new Thread( ReaderThread ); - this.readerThread.Name = this.Config.Server + " IRC reader thread"; + this.readerThread = new Thread( ReaderThread ) + { + Name = this.Config.Server + " IRC reader thread" + }; this.readerThread.Start(); // Per RFC-2812, the server password sets a "connection password". @@ -258,8 +238,7 @@ public void Connect() // Therefore, this is the first command that gets sent. if( string.IsNullOrEmpty( this.Config.ServerPassword ) == false ) { - this.ircWriter.WriteLine( "PASS {0}", this.Config.ServerPassword ); - this.ircWriter.Flush(); + this.connection.WriteLine( "PASS {0}", this.Config.ServerPassword ); Thread.Sleep( this.Config.RateLimit ); } else @@ -271,21 +250,18 @@ public void Connect() // This command is used at the beginning of a connection to specify the username, // real name and initial user modes of the connecting client. // may contain spaces, and thus must be prefixed with a colon. - this.ircWriter.WriteLine( "USER {0} 0 * :{1}", this.Config.UserName, this.Config.RealName ); - this.ircWriter.Flush(); + this.connection.WriteLine( "USER {0} 0 * :{1}", this.Config.UserName, this.Config.RealName ); Thread.Sleep( this.Config.RateLimit ); // NICK - this.ircWriter.WriteLine( "NICK {0}", this.Config.Nick ); - this.ircWriter.Flush(); + this.connection.WriteLine( "NICK {0}", this.Config.Nick ); Thread.Sleep( this.Config.RateLimit ); // If the server has a NickServ service, tell nickserv our password // so it registers our bot and does not change its nickname on us. if( string.IsNullOrEmpty( this.Config.NickServPassword ) == false ) { - this.ircWriter.WriteLine( "PRIVMSG NickServ :IDENTIFY {0}", this.Config.NickServPassword ); - this.ircWriter.Flush(); + this.connection.WriteLine( "PRIVMSG NickServ :IDENTIFY {0}", this.Config.NickServPassword ); Thread.Sleep( this.Config.RateLimit ); } else @@ -304,8 +280,7 @@ public void Connect() // If channel does not exist it will be created. foreach( string channel in this.Config.Channels ) { - this.ircWriter.WriteLine( "JOIN {0}", channel ); - this.ircWriter.Flush(); + this.connection.WriteLine( "JOIN {0}", channel ); this.AddCoreEvent( ChaskisCoreEvents.JoinChannel + " " + channel ); Thread.Sleep( this.Config.RateLimit ); @@ -382,14 +357,13 @@ private void SendMessageHelper( string line, string channel ) // CanWrite can return true, this thread can be preempted, // and a thread that disconnects the connection runs. Now, when this thread runs again, we try to write // to a socket that is closed which is a problem. - if( ( this.connection == null ) || ( this.connection.Connected == false ) ) + if( ( this.connection == null ) || ( this.connection.IsConnected == false ) ) { return; } // PRIVMSG < msgtarget > < message > - this.ircWriter.WriteLine( "PRIVMSG {0} :{1}", channel, line ); - this.ircWriter.Flush(); + this.connection.WriteLine( "PRIVMSG {0} :{1}", channel, line ); } } ); @@ -482,13 +456,12 @@ public void SendRawCmd( string cmd ) { lock( this.ircWriterLock ) { - if( ( this.connection == null ) || ( this.connection.Connected == false ) ) + if( ( this.connection == null ) || ( this.connection.IsConnected == false ) ) { return; } - this.ircWriter.WriteLine( cmd ); - this.ircWriter.Flush(); + this.connection.WriteLine( cmd ); } } ); @@ -554,7 +527,7 @@ public void Disconnect() // the writer is writing. lock( this.ircWriterLock ) { - this.ircReader.Close(); + this.connection.Disconnect(); } // - Wait for the reader thread to exit. @@ -568,6 +541,7 @@ public void Disconnect() } this.reconnector.Dispose(); + this.connection.Dispose(); StaticLogger.Log.WriteLine( "Disconnect Complete." ); } @@ -580,13 +554,7 @@ private void DisconnectHelper() { this.AddCoreEvent( ChaskisCoreEvents.DisconnectInProgress ); - this.connection.Close(); - - // Reset everything to null. - this.ircWriter = null; - this.ircReader = null; - this.sslStream = null; - this.connection = null; + this.connection.Disconnect(); // We are not connected. this.IsConnected = false; @@ -706,7 +674,7 @@ private void ReaderThread() try { // ReadLine blocks until we call Close() on the underlying stream. - string s = this.ircReader.ReadLine(); + string s = this.connection.ReadLine(); if( ( string.IsNullOrWhiteSpace( s ) == false ) && ( string.IsNullOrEmpty( s ) == false ) ) { // If KeepReading is set to false, we want this thread to exit. @@ -754,10 +722,12 @@ private void ReaderThread() catch( Exception err ) { // Unexpected exception occurred. The connection probably dropped. - // Nothing we can do now except to attempt to try again. + // Nothing we can do now except to wait for the watch dog to trigger a reconnect.. StaticLogger.Log.ErrorWriteLine( "IRC Reader Thread caught unexpected exception:" + Environment.NewLine + err + Environment.NewLine + "Wait for watchdog to reconnect..." ); + + return; } } // End While } @@ -786,7 +756,7 @@ private void AttemptReconnect() // We don't want any events to run and try to write to a closed connection. lock( this.ircWriterLock ) { - this.ircReader.Close(); // Close the IRC Stream. This also closes the writer as well. + this.connection.Disconnect(); DisconnectHelper(); } // Bad news is when we release the lock, any event that wants to write to the IRC Channel diff --git a/Chaskis/ChaskisCore/IrcMac.cs b/Chaskis/ChaskisCore/IrcMac.cs new file mode 100644 index 00000000..31481ec0 --- /dev/null +++ b/Chaskis/ChaskisCore/IrcMac.cs @@ -0,0 +1,182 @@ +// +// Copyright Seth Hendrick 2016-2019. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using SethCS.Basic; + +namespace Chaskis.Core +{ + public interface IIrcMac : IDisposable + { + // ---------------- Properties ---------------- + + bool IsConnected { get; } + + // ---------------- Functions ---------------- + + void Connect(); + + void Disconnect(); + + void WriteLine( string str ); + + void WriteLine( string str, params object[] objs ); + + string ReadLine(); + } + + /// + /// This is the Media Access Control (MAC) layer of an IRC Connection. + /// This is responsible for controlling how Chaskis.Core gains access to the socket and + /// transmit or receive data. + /// + public class IrcMac : IIrcMac + { + // ---------------- Fields ---------------- + + private readonly IIrcConfig config; + + private readonly GenericLogger log; + + private TcpClient connection; + private SslStream sslStream; + private StreamWriter ircWriter; + private StreamReader ircReader; + + private bool isDisposed; + + // ---------------- Constructor ---------------- + + public IrcMac( IIrcConfig config, GenericLogger log ) + { + this.config = config; + this.log = log; + this.isDisposed = false; + } + + // ---------------- Properties ---------------- + + public bool IsConnected + { + get + { + return + ( this.connection != null ) && + this.connection.Connected; + } + } + + // ---------------- Functions ---------------- + + public void Connect() + { + this.DisposeCheck(); + if( this.connection != null ) + { + throw new InvalidOperationException( "Already connected! can not connect again!" ); + } + + this.connection = new TcpClient( this.config.Server, this.config.Port ); + Stream stream; + if( this.config.UseSsl ) + { + this.log.WriteLine( "Using SSL Connection." ); + this.sslStream = new SslStream( this.connection.GetStream() ); + this.sslStream.AuthenticateAsClient( this.config.Server ); + stream = this.sslStream; + } + else + { + this.log.WriteLine( "WARNING! Using plain text connection." ); + stream = this.connection.GetStream(); + } + + this.ircReader = new StreamReader( stream ); + this.ircWriter = new StreamWriter( stream ); + } + + /// + /// Closes the connection. You are able to call + /// again to reopen the connection. + /// + /// No-op if we are already disconnected. + /// + public void Disconnect() + { + this.DisposeCheck(); + this.connection?.Close(); + + this.connection?.Dispose(); + this.sslStream?.Dispose(); + this.ircReader?.Dispose(); + this.ircWriter?.Dispose(); + + this.connection = null; + this.sslStream = null; + this.ircReader = null; + this.ircWriter = null; + } + + /// + /// Once called, you must create a new instance of this object. Calling + /// or any other function will throw an + /// + public void Dispose() + { + try + { + this.Disconnect(); + } + catch( Exception e ) + { + this.log.ErrorWriteLine( "Error when disconnecting:" + Environment.NewLine + e.ToString() ); + } + finally + { + this.isDisposed = true; + } + } + + /// + /// Writes the given sting to the IRC server. + /// Not thread safe. + /// + public void WriteLine( string str ) + { + this.DisposeCheck(); + + this.ircWriter.WriteLine( str ); + this.ircWriter.Flush(); + } + + public void WriteLine( string str, params object[] objs ) + { + this.DisposeCheck(); + + this.ircWriter.WriteLine( str, objs ); + this.ircWriter.Flush(); + } + + public string ReadLine() + { + this.DisposeCheck(); + + return this.ircReader.ReadLine(); + } + + private void DisposeCheck() + { + if( this.isDisposed ) + { + throw new ObjectDisposedException( nameof( IrcMac ) ); + } + } + } +}