|
|
@ -2,10 +2,8 @@
|
|
|
|
// in file 'LICENSE', which is part of this source code package.
|
|
|
|
// in file 'LICENSE', which is part of this source code package.
|
|
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
using System;
|
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Threading;
|
|
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
using SteamKit2.CDN;
|
|
|
|
using SteamKit2.CDN;
|
|
|
|
|
|
|
|
|
|
|
@ -16,139 +14,80 @@ namespace DepotDownloader
|
|
|
|
/// </summary>
|
|
|
|
/// </summary>
|
|
|
|
class CDNClientPool
|
|
|
|
class CDNClientPool
|
|
|
|
{
|
|
|
|
{
|
|
|
|
private const int ServerEndpointMinimumSize = 8;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private readonly Steam3Session steamSession;
|
|
|
|
private readonly Steam3Session steamSession;
|
|
|
|
private readonly uint appId;
|
|
|
|
private readonly uint appId;
|
|
|
|
public Client CDNClient { get; }
|
|
|
|
public Client CDNClient { get; }
|
|
|
|
public Server ProxyServer { get; private set; }
|
|
|
|
public Server ProxyServer { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
private readonly ConcurrentStack<Server> activeConnectionPool = [];
|
|
|
|
private readonly List<Server> servers = [];
|
|
|
|
private readonly BlockingCollection<Server> availableServerEndpoints = [];
|
|
|
|
private int nextServer;
|
|
|
|
|
|
|
|
|
|
|
|
private readonly AutoResetEvent populatePoolEvent = new(true);
|
|
|
|
|
|
|
|
private readonly Task monitorTask;
|
|
|
|
|
|
|
|
private readonly CancellationTokenSource shutdownToken = new();
|
|
|
|
|
|
|
|
public CancellationTokenSource ExhaustedToken { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public CDNClientPool(Steam3Session steamSession, uint appId)
|
|
|
|
public CDNClientPool(Steam3Session steamSession, uint appId)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
this.steamSession = steamSession;
|
|
|
|
this.steamSession = steamSession;
|
|
|
|
this.appId = appId;
|
|
|
|
this.appId = appId;
|
|
|
|
CDNClient = new Client(steamSession.steamClient);
|
|
|
|
CDNClient = new Client(steamSession.steamClient);
|
|
|
|
|
|
|
|
|
|
|
|
monitorTask = Task.Factory.StartNew(ConnectionPoolMonitorAsync).Unwrap();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Shutdown()
|
|
|
|
public async Task UpdateServerList()
|
|
|
|
{
|
|
|
|
{
|
|
|
|
shutdownToken.Cancel();
|
|
|
|
var servers = await this.steamSession.steamContent.GetServersForSteamPipe();
|
|
|
|
monitorTask.Wait();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<IReadOnlyCollection<Server>> FetchBootstrapServerListAsync()
|
|
|
|
ProxyServer = servers.Where(x => x.UseAsProxy).FirstOrDefault();
|
|
|
|
{
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var cdnServers = await this.steamSession.steamContent.GetServersForSteamPipe();
|
|
|
|
|
|
|
|
if (cdnServers != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return cdnServers;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
Console.WriteLine("Failed to retrieve content server list: {0}", ex.Message);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
var weightedCdnServers = servers
|
|
|
|
}
|
|
|
|
.Where(server =>
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var isEligibleForApp = server.AllowedAppIds.Length == 0 || server.AllowedAppIds.Contains(appId);
|
|
|
|
|
|
|
|
return isEligibleForApp && (server.Type == "SteamCache" || server.Type == "CDN");
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.Select(server =>
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
AccountSettingsStore.Instance.ContentServerPenalty.TryGetValue(server.Host, out var penalty);
|
|
|
|
|
|
|
|
|
|
|
|
private async Task ConnectionPoolMonitorAsync()
|
|
|
|
return (server, penalty);
|
|
|
|
{
|
|
|
|
})
|
|
|
|
var didPopulate = false;
|
|
|
|
.OrderBy(pair => pair.penalty).ThenBy(pair => pair.server.WeightedLoad);
|
|
|
|
|
|
|
|
|
|
|
|
while (!shutdownToken.IsCancellationRequested)
|
|
|
|
foreach (var (server, weight) in weightedCdnServers)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
populatePoolEvent.WaitOne(TimeSpan.FromSeconds(1));
|
|
|
|
for (var i = 0; i < server.NumEntries; i++)
|
|
|
|
|
|
|
|
|
|
|
|
// We want the Steam session so we can take the CellID from the session and pass it through to the ContentServer Directory Service
|
|
|
|
|
|
|
|
if (availableServerEndpoints.Count < ServerEndpointMinimumSize && steamSession.steamClient.IsConnected)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var servers = await FetchBootstrapServerListAsync().ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (servers == null || servers.Count == 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ExhaustedToken?.Cancel();
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ProxyServer = servers.Where(x => x.UseAsProxy).FirstOrDefault();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var weightedCdnServers = servers
|
|
|
|
|
|
|
|
.Where(server =>
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var isEligibleForApp = server.AllowedAppIds.Length == 0 || server.AllowedAppIds.Contains(appId);
|
|
|
|
|
|
|
|
return isEligibleForApp && (server.Type == "SteamCache" || server.Type == "CDN");
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.Select(server =>
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
AccountSettingsStore.Instance.ContentServerPenalty.TryGetValue(server.Host, out var penalty);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (server, penalty);
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.OrderBy(pair => pair.penalty).ThenBy(pair => pair.server.WeightedLoad);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var (server, weight) in weightedCdnServers)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
for (var i = 0; i < server.NumEntries; i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
availableServerEndpoints.Add(server);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
didPopulate = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else if (availableServerEndpoints.Count == 0 && !steamSession.steamClient.IsConnected && didPopulate)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
ExhaustedToken?.Cancel();
|
|
|
|
this.servers.Add(server);
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private Server BuildConnection(CancellationToken token)
|
|
|
|
if (this.servers.Count == 0)
|
|
|
|
{
|
|
|
|
|
|
|
|
if (availableServerEndpoints.Count < ServerEndpointMinimumSize)
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
populatePoolEvent.Set();
|
|
|
|
throw new Exception("Failed to retrieve any download servers.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return availableServerEndpoints.Take(token);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Server GetConnection(CancellationToken token)
|
|
|
|
public Server GetConnection()
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (!activeConnectionPool.TryPop(out var connection))
|
|
|
|
return servers[nextServer % servers.Count];
|
|
|
|
{
|
|
|
|
|
|
|
|
connection = BuildConnection(token);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return connection;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void ReturnConnection(Server server)
|
|
|
|
public void ReturnConnection(Server server)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (server == null) return;
|
|
|
|
if (server == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
activeConnectionPool.Push(server);
|
|
|
|
// nothing to do, maybe remove from ContentServerPenalty?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void ReturnBrokenConnection(Server server)
|
|
|
|
public void ReturnBrokenConnection(Server server)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (server == null) return;
|
|
|
|
if (server == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Broken connections are not returned to the pool
|
|
|
|
lock (servers)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (servers[nextServer % servers.Count] == server)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
nextServer++;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: Add server to ContentServerPenalty
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|