Skip to content

Commit

Permalink
Huge /Api/Log update for #733
Browse files Browse the repository at this point in the history
  • Loading branch information
JustArchi committed Jan 31, 2018
1 parent 2ee810d commit 9e88617
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 19 deletions.
63 changes: 63 additions & 0 deletions ArchiSteamFarm/FixedSizeConcurrentQueue.cs
@@ -0,0 +1,63 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
//
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace ArchiSteamFarm {
internal sealed class FixedSizeConcurrentQueue<T> : IEnumerable<T> {
private readonly ConcurrentQueue<T> BackingQueue = new ConcurrentQueue<T>();

internal byte MaxCount {
get => _MaxCount;
set {
_MaxCount = value;

while ((BackingQueue.Count > MaxCount) && BackingQueue.TryDequeue(out _)) { }
}
}

private byte _MaxCount;

internal FixedSizeConcurrentQueue(byte maxCount) {
if (maxCount == 0) {
throw new ArgumentNullException(nameof(maxCount));
}

MaxCount = maxCount;
}

public IEnumerator<T> GetEnumerator() => BackingQueue.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

internal void Enqueue(T obj) {
BackingQueue.Enqueue(obj);

if (BackingQueue.Count <= MaxCount) {
return;
}

BackingQueue.TryDequeue(out _);
}
}
}
80 changes: 80 additions & 0 deletions ArchiSteamFarm/HistoryTarget.cs
@@ -0,0 +1,80 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
//
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NLog;
using NLog.Targets;

namespace ArchiSteamFarm {
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
[Target(TargetName)]
internal sealed class HistoryTarget : TargetWithLayout {
internal const string TargetName = "History";

private const byte DefaultMaxCount = 10;

internal IEnumerable<string> ArchivedMessages => HistoryQueue;

private readonly FixedSizeConcurrentQueue<string> HistoryQueue = new FixedSizeConcurrentQueue<string>(DefaultMaxCount);

// This is NLog config property, it must have public get() and set() capabilities
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public byte MaxCount {
get => HistoryQueue.MaxCount;
set {
if (value == 0) {
ASF.ArchiLogger.LogNullError(nameof(value));
return;
}

HistoryQueue.MaxCount = value;
}
}

// This constructor is intentionally public, as NLog uses it for creating targets
// It must stay like this as we want to have our targets defined in our NLog.config
public HistoryTarget(string name = null) => Name = name;

protected override void Write(LogEventInfo logEvent) {
if (logEvent == null) {
ASF.ArchiLogger.LogNullError(nameof(logEvent));
return;
}

base.Write(logEvent);

string message = Layout.Render(logEvent);

HistoryQueue.Enqueue(message);
NewHistoryEntry?.Invoke(this, new NewHistoryEntryArgs(message));
}

internal event EventHandler<NewHistoryEntryArgs> NewHistoryEntry;

internal sealed class NewHistoryEntryArgs : EventArgs {
internal readonly string Message;

internal NewHistoryEntryArgs(string message) => Message = message ?? throw new ArgumentNullException(nameof(message));
}
}
}
91 changes: 77 additions & 14 deletions ArchiSteamFarm/IPC.cs
Expand Up @@ -39,6 +39,8 @@ namespace ArchiSteamFarm {
internal static class IPC {
internal static bool IsRunning => IsHandlingRequests || IsListening;

private static readonly ConcurrentHashSet<WebSocket> ActiveLogWebSockets = new ConcurrentHashSet<WebSocket>();

private static readonly HashSet<string> CompressableContentTypes = new HashSet<string> {
"application/javascript",
"text/css",
Expand Down Expand Up @@ -69,9 +71,25 @@ internal static class IPC {
}
}

private static HistoryTarget HistoryTarget;
private static HttpListener HttpListener;
private static bool IsHandlingRequests;

internal static void OnNewHistoryTarget(HistoryTarget historyTarget) {
if (historyTarget == null) {
ASF.ArchiLogger.LogNullError(nameof(historyTarget));
return;
}

if (HistoryTarget != null) {
HistoryTarget.NewHistoryEntry -= OnNewHistoryEntry;
HistoryTarget = null;
}

historyTarget.NewHistoryEntry += OnNewHistoryEntry;
HistoryTarget = historyTarget;
}

internal static void Start(string host, ushort port) {
if (string.IsNullOrEmpty(host) || (port == 0)) {
ASF.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(port));
Expand Down Expand Up @@ -106,6 +124,7 @@ internal static class IPC {
return;
}

Logging.InitHistoryLogger();
Utilities.StartBackgroundFunction(HandleRequests);
ASF.ArchiLogger.LogGenericInfo(Strings.IPCReady);
}
Expand Down Expand Up @@ -416,17 +435,34 @@ internal static class IPC {
try {
HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null).ConfigureAwait(false);

while (webSocketContext.WebSocket.State == WebSocketState.Open) {
const string testMessage = "WS works! In future there will be ASF log in JSON here";
await webSocketContext.WebSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(testMessage)), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);
await Task.Delay(3000).ConfigureAwait(false);
ActiveLogWebSockets.Add(webSocketContext.WebSocket);

try {
// Push initial history if available
if (HistoryTarget != null) {
await Task.WhenAll(HistoryTarget.ArchivedMessages.Select(archivedMessage => PostLogUpdate(webSocketContext.WebSocket, archivedMessage))).ConfigureAwait(false);
}

while (webSocketContext.WebSocket.State == WebSocketState.Open) {
WebSocketReceiveResult result = await webSocketContext.WebSocket.ReceiveAsync(new byte[0], CancellationToken.None).ConfigureAwait(false);

if (result.MessageType != WebSocketMessageType.Close) {
await webSocketContext.WebSocket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "You're not supposed to be sending any message but Close!", CancellationToken.None);
break;
}

await webSocketContext.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false);
break;
}
} finally {
ActiveLogWebSockets.Remove(webSocketContext.WebSocket);
}
} catch (WebSocketException) {
// Ignored, request is no longer valid

return true;
} catch (WebSocketException e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
return true;
}

return true;
}

private static async Task<bool> HandleApiStructure(HttpListenerRequest request, HttpListenerResponse response, string[] arguments, byte argumentsIndex) {
Expand Down Expand Up @@ -641,6 +677,33 @@ internal static class IPC {
}
}

private static async void OnNewHistoryEntry(object sender, HistoryTarget.NewHistoryEntryArgs newHistoryEntryArgs) {
if ((sender == null) || (newHistoryEntryArgs == null)) {
ASF.ArchiLogger.LogNullError(nameof(sender) + " || " + nameof(newHistoryEntryArgs));
return;
}

await Task.WhenAll(ActiveLogWebSockets.Where(webSocket => webSocket.State == WebSocketState.Open).Select(webSocket => PostLogUpdate(webSocket, newHistoryEntryArgs.Message))).ConfigureAwait(false);
}

private static async Task PostLogUpdate(WebSocket webSocket, string message) {
if ((webSocket == null) || string.IsNullOrEmpty(message)) {
ASF.ArchiLogger.LogNullError(nameof(webSocket) + " || " + nameof(message));
return;
}

if (webSocket.State != WebSocketState.Open) {
return;
}

try {
string response = JsonConvert.SerializeObject(new GenericResponse(true, "OK", message));
await webSocket.SendAsync(Encoding.UTF8.GetBytes(response), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);
} catch (WebSocketException e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
}
}

private static async Task ResponseBase(HttpListenerRequest request, HttpListenerResponse response, byte[] content, HttpStatusCode statusCode = HttpStatusCode.OK) {
if ((request == null) || (response == null) || (content == null) || (content.Length == 0)) {
ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(content));
Expand Down Expand Up @@ -682,8 +745,8 @@ internal static class IPC {

response.ContentLength64 = content.Length;
await response.OutputStream.WriteAsync(content, 0, content.Length).ConfigureAwait(false);
} catch (ObjectDisposedException) {
// Ignored, request is no longer valid
} catch (ObjectDisposedException e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
}
}

Expand All @@ -700,8 +763,8 @@ internal static class IPC {
await ResponseBase(request, response, content).ConfigureAwait(false);
} catch (FileNotFoundException) {
await ResponseStatusCode(request, response, HttpStatusCode.NotFound).ConfigureAwait(false);
} catch (ObjectDisposedException) {
// Ignored, request is no longer valid
} catch (ObjectDisposedException e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
await ResponseStatusCode(request, response, HttpStatusCode.ServiceUnavailable).ConfigureAwait(false);
Expand Down Expand Up @@ -751,8 +814,8 @@ internal static class IPC {

byte[] content = response.ContentEncoding.GetBytes(text + Environment.NewLine);
await ResponseBase(request, response, content, statusCode).ConfigureAwait(false);
} catch (ObjectDisposedException) {
// Ignored, request is no longer valid
} catch (ObjectDisposedException e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
}
}

Expand Down
22 changes: 21 additions & 1 deletion ArchiSteamFarm/Logging.cs
Expand Up @@ -50,7 +50,7 @@ internal static class Logging {
}
}

internal static void InitLoggers() {
internal static void InitCoreLoggers() {
if (LogManager.Configuration != null) {
IsUsingCustomConfiguration = true;
InitConsoleLoggers();
Expand Down Expand Up @@ -81,6 +81,21 @@ internal static class Logging {
InitConsoleLoggers();
}

internal static void InitHistoryLogger() {
if (IsUsingCustomConfiguration || (LogManager.Configuration == null)) {
return;
}

// TODO: We could use some nice HTML layout for this
HistoryTarget historyTarget = new HistoryTarget("History") { Layout = GeneralLayout };

LogManager.Configuration.AddTarget(historyTarget);
LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, historyTarget));

LogManager.ReconfigExistingLoggers();
IPC.OnNewHistoryTarget(historyTarget);
}

internal static void OnUserInputEnd() {
IsWaitingForUserInput = false;

Expand Down Expand Up @@ -127,6 +142,11 @@ internal static class Logging {
if (IsWaitingForUserInput) {
OnUserInputStart();
}

HistoryTarget historyTarget = (HistoryTarget) LogManager.Configuration.AllTargets.FirstOrDefault(target => target is HistoryTarget);
if (historyTarget != null) {
IPC.OnNewHistoryTarget(historyTarget);
}
}
}
}
5 changes: 3 additions & 2 deletions ArchiSteamFarm/Program.cs
Expand Up @@ -181,7 +181,8 @@ internal static class Program {
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;

// We must register our logging target as soon as possible
// We must register our logging targets as soon as possible
Target.Register<HistoryTarget>(HistoryTarget.TargetName);
Target.Register<SteamTarget>(SteamTarget.TargetName);

InitCore(args);
Expand Down Expand Up @@ -251,7 +252,7 @@ internal static class Program {
ParsePreInitArgs(args);
}

Logging.InitLoggers();
Logging.InitCoreLoggers();
}

private static async Task InitGlobalConfigAndLanguage() {
Expand Down
7 changes: 5 additions & 2 deletions ArchiSteamFarm/SteamTarget.cs
Expand Up @@ -43,9 +43,12 @@ internal sealed class SteamTarget : TargetWithLayout {
public ulong SteamID { get; set; }

// This constructor is intentionally public, as NLog uses it for creating targets
// It must stay like this as we want to have SteamTargets defined in our NLog.config
// It must stay like this as we want to have our targets defined in our NLog.config
// Keeping date in default layout also doesn't make much sense, so we remove it by default
public SteamTarget() => Layout = "${level:uppercase=true}|${logger}|${message}";
public SteamTarget(string name = null) {
Name = name;
Layout = "${level:uppercase=true}|${logger}|${message}";
}

protected override async void Write(LogEventInfo logEvent) {
if (logEvent == null) {
Expand Down

0 comments on commit 9e88617

Please sign in to comment.