From 0e8b48933940f64466678b1f8293505323d6e1f5 Mon Sep 17 00:00:00 2001 From: Ryan Stecker Date: Mon, 14 Sep 2015 15:53:57 -0500 Subject: [PATCH 01/25] We need .NET 4.5 for these goodies. --- SteamKit2/SteamKit2/SteamKit2.csproj | 4 +++- SteamKit2/SteamKit2/packages.config | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/SteamKit2/SteamKit2/SteamKit2.csproj b/SteamKit2/SteamKit2/SteamKit2.csproj index a093ff602..2bb1276dc 100644 --- a/SteamKit2/SteamKit2/SteamKit2.csproj +++ b/SteamKit2/SteamKit2/SteamKit2.csproj @@ -10,7 +10,7 @@ Properties SteamKit2 SteamKit2 - v4.0 + v4.5 512 @@ -47,6 +47,7 @@ AllRules.ruleset bin\Debug\SteamKit2.XML true + false pdbonly @@ -59,6 +60,7 @@ AllRules.ruleset bin\Release\SteamKit2.xml true + false diff --git a/SteamKit2/SteamKit2/packages.config b/SteamKit2/SteamKit2/packages.config index b32f14f4f..9dbdc9cd9 100644 --- a/SteamKit2/SteamKit2/packages.config +++ b/SteamKit2/SteamKit2/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file From 8b2fe8378e44fa0d5264c4a61c9f50249a366feb Mon Sep 17 00:00:00 2001 From: Ryan Stecker Date: Mon, 14 Sep 2015 15:55:00 -0500 Subject: [PATCH 02/25] Add awaitable interface for job based messages. --- .../Steam/SteamClient/SteamClient.cs | 28 ++++- SteamKit2/SteamKit2/SteamKit2.csproj | 1 + SteamKit2/SteamKit2/Types/GlobalID.cs | 53 --------- SteamKit2/SteamKit2/Types/JobID.cs | 108 ++++++++++++++++++ 4 files changed, 135 insertions(+), 55 deletions(-) create mode 100644 SteamKit2/SteamKit2/Types/JobID.cs diff --git a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs index 98a62156c..8b9d2631d 100644 --- a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs +++ b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs @@ -6,12 +6,13 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Net.Sockets; using System.Threading; -using SteamKit2.Internal; -using System.Diagnostics; using ProtoBuf; +using SteamKit2.Internal; namespace SteamKit2 { @@ -31,6 +32,8 @@ public sealed partial class SteamClient : CMClient Dictionary> dispatchMap; + ConcurrentDictionary asyncJobs; + /// /// Initializes a new instance of the class with a specific connection type. @@ -70,6 +73,8 @@ public SteamClient( ProtocolType type = ProtocolType.Tcp ) { EMsg.ClientCMList, HandleCMList }, { EMsg.ClientServerList, HandleServerList }, }; + + asyncJobs = new ConcurrentDictionary(); } @@ -245,6 +250,8 @@ public void PostCallback( CallbackMsg msg ) callbackQueue.Enqueue( msg ); Monitor.Pulse( callbackLock ); } + + TryCompleteJob( msg.JobID, msg ); } #endregion @@ -266,6 +273,23 @@ public JobID GetNextJobID() }; } + internal void StartJob( AsyncJob asyncJob ) + { + asyncJobs.TryAdd( asyncJob, asyncJob ); + } + void TryCompleteJob( JobID jobId, object callback ) + { + AsyncJob asyncJob; + + if ( !asyncJobs.TryRemove( jobId, out asyncJob ) ) + { + // not a job we are tracking ourselves, can ignore it + return; + } + + asyncJob.Complete( callback ); + } + /// /// Called when a client message is received from the network. diff --git a/SteamKit2/SteamKit2/SteamKit2.csproj b/SteamKit2/SteamKit2/SteamKit2.csproj index 2bb1276dc..8acdcbd0f 100644 --- a/SteamKit2/SteamKit2/SteamKit2.csproj +++ b/SteamKit2/SteamKit2/SteamKit2.csproj @@ -146,6 +146,7 @@ + diff --git a/SteamKit2/SteamKit2/Types/GlobalID.cs b/SteamKit2/SteamKit2/Types/GlobalID.cs index 8fbf0e8ae..8a84ab95c 100644 --- a/SteamKit2/SteamKit2/Types/GlobalID.cs +++ b/SteamKit2/SteamKit2/Types/GlobalID.cs @@ -224,59 +224,6 @@ public override string ToString() } - /// - /// Represents an identifier of a network task known as a job. - /// - public sealed class JobID : GlobalID - { - /// - /// Represents an invalid JobID. - /// - public static readonly JobID Invalid = new JobID(); - - - /// - /// Initializes a new instance of the class. - /// - public JobID() - : base() - { - } - /// - /// Initializes a new instance of the class. - /// - /// The Job ID to initialize this instance with. - public JobID( ulong jobId ) - : base( jobId ) - { - } - - - /// - /// Performs an implicit conversion from to . - /// - /// The Job ID. - /// - /// The result of the conversion. - /// - public static implicit operator ulong( JobID jobId ) - { - return jobId.Value; - } - - /// - /// Performs an implicit conversion from to . - /// - /// The Job ID. - /// - /// The result of the conversion. - /// - public static implicit operator JobID( ulong jobId ) - { - return new JobID( jobId ); - } - } - /// /// Represents a single unique handle to a piece of User Generated Content. /// diff --git a/SteamKit2/SteamKit2/Types/JobID.cs b/SteamKit2/SteamKit2/Types/JobID.cs new file mode 100644 index 000000000..e85a6ab94 --- /dev/null +++ b/SteamKit2/SteamKit2/Types/JobID.cs @@ -0,0 +1,108 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'license.txt', which is part of this source code package. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace SteamKit2 +{ + /// + /// Represents an identifier of a network task known as a job. + /// + public class JobID : GlobalID + { + /// + /// Represents an invalid JobID. + /// + public static readonly JobID Invalid = new JobID(); + + + /// + /// Initializes a new instance of the class. + /// + public JobID() + : base() + { + } + /// + /// Initializes a new instance of the class. + /// + /// The Job ID to initialize this instance with. + public JobID( ulong jobId ) + : base( jobId ) + { + } + + + /// + /// Performs an implicit conversion from to . + /// + /// The Job ID. + /// + /// The result of the conversion. + /// + public static implicit operator ulong ( JobID jobId ) + { + return jobId.Value; + } + + /// + /// Performs an implicit conversion from to . + /// + /// The Job ID. + /// + /// The result of the conversion. + /// + public static implicit operator JobID( ulong jobId ) + { + return new JobID( jobId ); + } + } + + public abstract class AsyncJob : JobID + { + public AsyncJob( SteamClient client, ulong jobId ) + : base( jobId ) + { + client.StartJob( this ); + } + + + internal abstract void Complete( object callback ); + } + + public sealed class AsyncJob : AsyncJob + where T : CallbackMsg + { + TaskCompletionSource tcs; + + + public AsyncJob( SteamClient client, ulong jobId ) + : base( client, jobId ) + { + tcs = new TaskCompletionSource(); + } + + + public Task ToTask() + { + return tcs.Task; + } + + public TaskAwaiter GetAwaiter() + { + return ToTask().GetAwaiter(); + } + + internal override void Complete( object callback ) + { + tcs.TrySetResult( (T)callback ); + } + } +} From 76000303b87c801d97975b7b7a277a33466f0d31 Mon Sep 17 00:00:00 2001 From: Ryan Stecker Date: Mon, 14 Sep 2015 15:55:28 -0500 Subject: [PATCH 03/25] Convert SteamMasterServer to awaitable jobs. --- .../Steam/Handlers/SteamMasterServer/SteamMasterServer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamMasterServer/SteamMasterServer.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamMasterServer/SteamMasterServer.cs index a4ec32788..2b8d6b7b0 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamMasterServer/SteamMasterServer.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamMasterServer/SteamMasterServer.cs @@ -65,7 +65,7 @@ internal SteamMasterServer() /// /// The details for the request. /// The Job ID of the request. This can be used to find the appropriate . - public JobID ServerQuery( QueryDetails details ) + public AsyncJob ServerQuery( QueryDetails details ) { var query = new ClientMsgProtobuf( EMsg.ClientGMSServerQuery ); query.SourceJobID = Client.GetNextJobID(); @@ -82,7 +82,7 @@ public JobID ServerQuery( QueryDetails details ) this.Client.Send( query ); - return query.SourceJobID; + return new AsyncJob( this.Client, query.SourceJobID ); } From 0a349a6d777d57707293545c74e9e9745472395f Mon Sep 17 00:00:00 2001 From: Ryan Stecker Date: Mon, 14 Sep 2015 16:12:34 -0500 Subject: [PATCH 04/25] Cancel pending jobs upon disconnect. --- SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs | 9 +++++++++ SteamKit2/SteamKit2/Types/JobID.cs | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs index 8b9d2631d..e392fcd66 100644 --- a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs +++ b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs @@ -341,6 +341,15 @@ protected override void OnClientMsgReceived( IPacketMsg packetMsg ) /// protected override void OnClientDisconnected( bool userInitiated ) { + // if we are disconnected, cancel all pending jobs + + foreach ( AsyncJob asyncJob in asyncJobs.Values ) + { + asyncJob.Complete( null ); + } + + asyncJobs.Clear(); + this.PostCallback( new DisconnectedCallback( userInitiated ) ); } diff --git a/SteamKit2/SteamKit2/Types/JobID.cs b/SteamKit2/SteamKit2/Types/JobID.cs index e85a6ab94..23c1edba9 100644 --- a/SteamKit2/SteamKit2/Types/JobID.cs +++ b/SteamKit2/SteamKit2/Types/JobID.cs @@ -100,8 +100,19 @@ public TaskAwaiter GetAwaiter() return ToTask().GetAwaiter(); } + internal override void Complete( object callback ) { + if ( callback == null ) + { + // if we're completing with a null callback object, this is a signal that the job has been cancelled + // without a valid result from the steam servers + + tcs.TrySetCanceled(); + + return; + } + tcs.TrySetResult( (T)callback ); } } From 0483251246f966ce921bdcd4774a4d80106cec1c Mon Sep 17 00:00:00 2001 From: Ryan Stecker Date: Mon, 14 Sep 2015 16:57:14 -0500 Subject: [PATCH 05/25] Periodically check and cancel any jobs that timeout. --- .../Steam/SteamClient/SteamClient.cs | 38 ++++++++++++++++--- SteamKit2/SteamKit2/Types/JobID.cs | 10 +++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs index e392fcd66..8b818d96c 100644 --- a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs +++ b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs @@ -33,6 +33,7 @@ public sealed partial class SteamClient : CMClient Dictionary> dispatchMap; ConcurrentDictionary asyncJobs; + ScheduledFunction jobTimeoutFunc; /// @@ -75,6 +76,8 @@ public SteamClient( ProtocolType type = ProtocolType.Tcp ) }; asyncJobs = new ConcurrentDictionary(); + + jobTimeoutFunc = new ScheduledFunction( CancelTimedoutJobs, TimeSpan.FromSeconds( 5 ) ); } @@ -289,6 +292,28 @@ void TryCompleteJob( JobID jobId, object callback ) asyncJob.Complete( callback ); } + void CancelPendingJobs() + { + foreach ( AsyncJob asyncJob in asyncJobs.Values ) + { + asyncJob.Complete( null ); + } + + asyncJobs.Clear(); + } + void CancelTimedoutJobs() + { + foreach ( AsyncJob job in asyncJobs.Values ) + { + if ( job.IsTimedout ) + { + job.Complete( null ); + + AsyncJob ignored; + asyncJobs.TryRemove( job, out ignored ); + } + } + } /// @@ -342,13 +367,9 @@ protected override void OnClientMsgReceived( IPacketMsg packetMsg ) protected override void OnClientDisconnected( bool userInitiated ) { // if we are disconnected, cancel all pending jobs + CancelPendingJobs(); - foreach ( AsyncJob asyncJob in asyncJobs.Values ) - { - asyncJob.Complete( null ); - } - - asyncJobs.Clear(); + jobTimeoutFunc.Stop(); this.PostCallback( new DisconnectedCallback( userInitiated ) ); } @@ -358,6 +379,11 @@ void HandleEncryptResult( IPacketMsg packetMsg ) { var encResult = new Msg( packetMsg ); + if ( encResult.Body.Result == EResult.OK ) + { + jobTimeoutFunc.Start(); + } + PostCallback( new ConnectedCallback( encResult.Body ) ); } diff --git a/SteamKit2/SteamKit2/Types/JobID.cs b/SteamKit2/SteamKit2/Types/JobID.cs index 23c1edba9..bc206b6ec 100644 --- a/SteamKit2/SteamKit2/Types/JobID.cs +++ b/SteamKit2/SteamKit2/Types/JobID.cs @@ -67,9 +67,19 @@ public JobID( ulong jobId ) public abstract class AsyncJob : JobID { + DateTime jobStart; + + internal bool IsTimedout + { + get { return DateTime.UtcNow >= jobStart + TimeSpan.FromMinutes( 1 ); } + } + + public AsyncJob( SteamClient client, ulong jobId ) : base( jobId ) { + jobStart = DateTime.UtcNow; + client.StartJob( this ); } From cc7035392ec653964f529f7c7f641342b39b9019 Mon Sep 17 00:00:00 2001 From: Ryan Stecker Date: Mon, 14 Sep 2015 18:16:43 -0500 Subject: [PATCH 06/25] Upgrade tests to xunit 2.0.0 --- SteamKit2/Tests/DebugLogFacts.cs | 1 + SteamKit2/Tests/SteamUserFacts.cs | 8 ++++++-- SteamKit2/Tests/Tests.csproj | 23 ++++++++++++++++++----- SteamKit2/Tests/packages.config | 10 +++++++--- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/SteamKit2/Tests/DebugLogFacts.cs b/SteamKit2/Tests/DebugLogFacts.cs index 79e4f3c05..e034d0056 100644 --- a/SteamKit2/Tests/DebugLogFacts.cs +++ b/SteamKit2/Tests/DebugLogFacts.cs @@ -1,5 +1,6 @@ using Xunit; using SteamKit2; +using Xunit.Sdk; namespace Tests { diff --git a/SteamKit2/Tests/SteamUserFacts.cs b/SteamKit2/Tests/SteamUserFacts.cs index da150ec62..25704711a 100644 --- a/SteamKit2/Tests/SteamUserFacts.cs +++ b/SteamKit2/Tests/SteamUserFacts.cs @@ -97,7 +97,7 @@ public void LogOnThrowsExceptionIfLoginKeyProvidedWithoutShouldRememberPassword( [Fact] public void LogOnDoesNotThrowExceptionIfUserNameAndPasswordProvided() { - Assert.DoesNotThrow(() => + var ex = Record.Exception(() => { Handler.LogOn(new SteamUser.LogOnDetails { @@ -105,12 +105,14 @@ public void LogOnDoesNotThrowExceptionIfUserNameAndPasswordProvided() Password = "def" }); }); + + Assert.Null( ex ); } [Fact] public void LogOnDoesNotThrowExceptionIfUserNameAndLoginKeyProvided() { - Assert.DoesNotThrow(() => + var ex = Record.Exception(() => { Handler.LogOn(new SteamUser.LogOnDetails { @@ -119,6 +121,8 @@ public void LogOnDoesNotThrowExceptionIfUserNameAndLoginKeyProvided() ShouldRememberPassword = true, }); }); + + Assert.Null( ex ); } [Fact] diff --git a/SteamKit2/Tests/Tests.csproj b/SteamKit2/Tests/Tests.csproj index 2dfa6eaa0..30e88cc29 100644 --- a/SteamKit2/Tests/Tests.csproj +++ b/SteamKit2/Tests/Tests.csproj @@ -1,5 +1,6 @@  + @@ -10,12 +11,13 @@ Properties Tests Tests - v4.0 + v4.5 512 ..\ true - bd026ff8 + + true @@ -25,6 +27,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -33,6 +36,7 @@ TRACE prompt 4 + false @@ -46,9 +50,17 @@ - - False - ..\packages\xunit.1.9.2\lib\net20\xunit.dll + + ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True + + + ..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True @@ -86,6 +98,7 @@ This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. +