2019-02-16 17:34:17 +01:00
// _ _ _ ____ _ _____
2018-09-08 00:05:23 +02:00
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
2019-01-14 19:11:17 +01:00
// |
2019-01-02 16:32:53 +01:00
// Copyright 2015-2019 Łukasz "JustArchi" Domeradzki
2018-09-08 00:05:23 +02:00
// Contact: JustArchi@JustArchi.net
2019-01-14 19:11:17 +01:00
// |
2018-09-08 00:05:23 +02:00
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
2019-01-14 19:11:17 +01:00
// |
2018-09-08 00:05:23 +02:00
// http://www.apache.org/licenses/LICENSE-2.0
2019-01-14 19:11:17 +01:00
// |
2018-09-08 00:05:23 +02:00
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Collections.Concurrent ;
2018-10-08 00:09:30 +02:00
using System.Collections.Generic ;
2018-09-08 00:05:23 +02:00
using System.Linq ;
2019-04-11 22:47:21 +02:00
using System.Net ;
2018-09-08 00:05:23 +02:00
using System.Net.WebSockets ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
using ArchiSteamFarm.IPC.Responses ;
using ArchiSteamFarm.Localization ;
2018-09-08 01:03:55 +02:00
using ArchiSteamFarm.NLog ;
2018-09-08 00:05:23 +02:00
using Microsoft.AspNetCore.Mvc ;
using Newtonsoft.Json ;
namespace ArchiSteamFarm.IPC.Controllers.Api {
2018-09-14 23:02:15 +02:00
[Route("Api/NLog")]
2018-10-08 02:49:34 +02:00
public sealed class NLogController : ArchiController {
2018-09-08 00:05:23 +02:00
private static readonly ConcurrentDictionary < WebSocket , SemaphoreSlim > ActiveLogWebSockets = new ConcurrentDictionary < WebSocket , SemaphoreSlim > ( ) ;
2018-10-06 05:06:29 +02:00
/// <summary>
2018-10-17 20:22:55 +02:00
/// Fetches ASF log in realtime.
2018-10-06 05:06:29 +02:00
/// </summary>
/// <remarks>
2018-10-17 20:22:55 +02:00
/// This API endpoint requires a websocket connection.
2018-10-06 05:06:29 +02:00
/// </remarks>
2018-09-08 00:05:23 +02:00
[HttpGet]
2019-04-11 22:47:21 +02:00
[ProducesResponseType(typeof(IEnumerable<GenericResponse<string>>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
2018-09-21 17:16:43 +02:00
public async Task < ActionResult > NLogGet ( ) {
2018-09-08 00:05:23 +02:00
if ( ! HttpContext . WebSockets . IsWebSocketRequest ) {
return BadRequest ( new GenericResponse ( false , string . Format ( Strings . WarningFailedWithError , nameof ( HttpContext . WebSockets . IsWebSocketRequest ) + ": " + HttpContext . WebSockets . IsWebSocketRequest ) ) ) ;
}
// From now on we can return only EmptyResult as the response stream is already being used by existing websocket connection
try {
2019-09-27 20:43:11 +02:00
using WebSocket webSocket = await HttpContext . WebSockets . AcceptWebSocketAsync ( ) . ConfigureAwait ( false ) ;
2018-12-15 00:27:15 +01:00
2019-09-27 20:43:11 +02:00
SemaphoreSlim sendSemaphore = new SemaphoreSlim ( 1 , 1 ) ;
2018-12-15 00:27:15 +01:00
2019-09-27 20:43:11 +02:00
if ( ! ActiveLogWebSockets . TryAdd ( webSocket , sendSemaphore ) ) {
sendSemaphore . Dispose ( ) ;
2018-09-08 00:05:23 +02:00
2019-09-27 20:43:11 +02:00
return new EmptyResult ( ) ;
}
2018-09-08 00:05:23 +02:00
2019-09-27 20:43:11 +02:00
try {
// Push initial history if available
if ( ArchiKestrel . HistoryTarget ! = null ) {
// ReSharper disable once AccessToDisposedClosure - we're waiting for completion with Task.WhenAll(), we're not going to exit using block
await Task . WhenAll ( ArchiKestrel . HistoryTarget . ArchivedMessages . Select ( archivedMessage = > PostLoggedMessageUpdate ( webSocket , sendSemaphore , archivedMessage ) ) ) . ConfigureAwait ( false ) ;
}
2018-12-15 00:27:15 +01:00
2019-09-27 20:43:11 +02:00
while ( webSocket . State = = WebSocketState . Open ) {
WebSocketReceiveResult result = await webSocket . ReceiveAsync ( new byte [ 0 ] , CancellationToken . None ) . ConfigureAwait ( false ) ;
2018-09-08 00:05:23 +02:00
2019-09-27 20:43:11 +02:00
if ( result . MessageType ! = WebSocketMessageType . Close ) {
await webSocket . CloseAsync ( WebSocketCloseStatus . InvalidMessageType , "You're not supposed to be sending any message but Close!" , CancellationToken . None ) . ConfigureAwait ( false ) ;
2018-12-15 00:27:15 +01:00
2018-09-08 00:05:23 +02:00
break ;
}
2019-09-27 20:43:11 +02:00
await webSocket . CloseAsync ( WebSocketCloseStatus . NormalClosure , "" , CancellationToken . None ) . ConfigureAwait ( false ) ;
break ;
}
} finally {
if ( ActiveLogWebSockets . TryRemove ( webSocket , out SemaphoreSlim closedSemaphore ) ) {
await closedSemaphore . WaitAsync ( ) . ConfigureAwait ( false ) ; // Ensure that our semaphore is truly closed by now
closedSemaphore . Dispose ( ) ;
2018-09-08 00:05:23 +02:00
}
}
} catch ( WebSocketException e ) {
ASF . ArchiLogger . LogGenericDebuggingException ( e ) ;
}
return new EmptyResult ( ) ;
}
internal static async void OnNewHistoryEntry ( object sender , HistoryTarget . NewHistoryEntryArgs newHistoryEntryArgs ) {
if ( ( sender = = null ) | | ( newHistoryEntryArgs = = null ) ) {
ASF . ArchiLogger . LogNullError ( nameof ( sender ) + " || " + nameof ( newHistoryEntryArgs ) ) ;
2018-12-15 00:27:15 +01:00
2018-09-08 00:05:23 +02:00
return ;
}
if ( ActiveLogWebSockets . Count = = 0 ) {
return ;
}
string json = JsonConvert . SerializeObject ( new GenericResponse < string > ( newHistoryEntryArgs . Message ) ) ;
await Task . WhenAll ( ActiveLogWebSockets . Where ( kv = > kv . Key . State = = WebSocketState . Open ) . Select ( kv = > PostLoggedJsonUpdate ( kv . Key , kv . Value , json ) ) ) . ConfigureAwait ( false ) ;
}
private static async Task PostLoggedJsonUpdate ( WebSocket webSocket , SemaphoreSlim sendSemaphore , string json ) {
if ( ( webSocket = = null ) | | ( sendSemaphore = = null ) | | string . IsNullOrEmpty ( json ) ) {
ASF . ArchiLogger . LogNullError ( nameof ( webSocket ) + " || " + nameof ( sendSemaphore ) + " || " + nameof ( json ) ) ;
2018-12-15 00:27:15 +01:00
2018-09-08 00:05:23 +02:00
return ;
}
if ( webSocket . State ! = WebSocketState . Open ) {
return ;
}
await sendSemaphore . WaitAsync ( ) . ConfigureAwait ( false ) ;
try {
if ( webSocket . State ! = WebSocketState . Open ) {
return ;
}
await webSocket . SendAsync ( Encoding . UTF8 . GetBytes ( json ) , WebSocketMessageType . Text , true , CancellationToken . None ) . ConfigureAwait ( false ) ;
} catch ( WebSocketException e ) {
ASF . ArchiLogger . LogGenericDebuggingException ( e ) ;
} finally {
sendSemaphore . Release ( ) ;
}
}
private static async Task PostLoggedMessageUpdate ( WebSocket webSocket , SemaphoreSlim sendSemaphore , string loggedMessage ) {
if ( ( webSocket = = null ) | | ( sendSemaphore = = null ) | | string . IsNullOrEmpty ( loggedMessage ) ) {
ASF . ArchiLogger . LogNullError ( nameof ( webSocket ) + " || " + nameof ( sendSemaphore ) + " || " + nameof ( loggedMessage ) ) ;
2018-12-15 00:27:15 +01:00
2018-09-08 00:05:23 +02:00
return ;
}
if ( webSocket . State ! = WebSocketState . Open ) {
return ;
}
string response = JsonConvert . SerializeObject ( new GenericResponse < string > ( loggedMessage ) ) ;
await PostLoggedJsonUpdate ( webSocket , sendSemaphore , response ) . ConfigureAwait ( false ) ;
}
}
}