Skip to content

Commit

Permalink
Move single instance control to separate class, add more context info…
Browse files Browse the repository at this point in the history
… to logs
  • Loading branch information
matsakiv committed May 8, 2023
1 parent 7e882f0 commit bc47fcd
Show file tree
Hide file tree
Showing 15 changed files with 448 additions and 318 deletions.
222 changes: 69 additions & 153 deletions App.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

Expand All @@ -19,11 +13,9 @@
using Avalonia.Input.Platform;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;

using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Display;

using Atomex.Client.Desktop.Services;
using Atomex.Client.Desktop.ViewModels;
Expand All @@ -48,13 +40,21 @@ public class App : Application
public static MainWindowViewModel MainWindowViewModel;
public static Action<string> ConnectTezosDapp;

private SingleInstanceLoopbackService _singleInstanceLoopbackService;

public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
{
Environment.Exit(0);
return;
}

// set invariant culture by default
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// configure loggers
Expand All @@ -69,132 +69,75 @@ public override void OnFrameworkInitializationCompleted()
Log.Information("Setting startup data from URLOpened {Url}", args.Urls[0]);
};

if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
if (SingleInstanceLoopbackService.TrySendArgsToOtherInstance(desktop.Args, LoggerFactory.CreateLogger<SingleInstanceLoopbackService>()))
{
const int atomexTcpPort = 49531;
var appAlreadyRunning = true;

using var tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

try
{
tcpClient.Connect(IPAddress.Loopback, atomexTcpPort);
}
catch (SocketException)
{
appAlreadyRunning = false;
}

if (appAlreadyRunning)
{
if (desktop.Args.Length != 0)
{
var bytesArgs = Encoding.UTF8.GetBytes(desktop.Args[0]);
tcpClient.Send(bytesArgs);
Log.Information("Sending data to running app instance {Data}", desktop.Args[0]);
}

tcpClient.Disconnect(false);
tcpClient.Close();
Environment.Exit(0);
return;
}

TemplateService = new TemplateService();
Clipboard = AvaloniaLocator.Current.GetService<IClipboard>();

var currenciesProvider = new CurrenciesProvider(CurrenciesConfigurationString);
var symbolsProvider = new SymbolsProvider(SymbolsConfiguration);

var bitfinexQuotesProvider = new BitfinexQuotesProvider(
currencies: currenciesProvider
.GetCurrencies(Network.MainNet)
.GetOrderedPreset()
.Select(c => c.Name),
baseCurrency: QuotesProvider.Usd,
log: LoggerFactory.CreateLogger<BitfinexQuotesProvider>());

var tezToolsQuotesProvider = new TezToolsQuotesProvider(
log: LoggerFactory.CreateLogger<TezToolsQuotesProvider>());

var quotesProvider = new MultiSourceQuotesProvider(
log: LoggerFactory.CreateLogger<MultiSourceQuotesProvider>(),
bitfinexQuotesProvider, tezToolsQuotesProvider);

// init Atomex client app
AtomexApp = new AtomexApp(logger: LoggerFactory.CreateLogger("AtomexApp"))
.UseCurrenciesProvider(currenciesProvider)
.UseSymbolsProvider(symbolsProvider)
.UseCurrenciesUpdater(new CurrenciesUpdater(currenciesProvider))
.UseSymbolsUpdater(new SymbolsUpdater(symbolsProvider))
.UseQuotesProvider(quotesProvider);

var mainWindow = new MainWindow();
DialogService = new DialogService();

NotificationsService = new NotificationsService(AtomexApp, mainWindow.NotificationManager);
MainWindowViewModel = new MainWindowViewModel(AtomexApp, mainWindow);
mainWindow.DataContext = MainWindowViewModel;
desktop.Exit += OnExit;

if (desktop.Args.Length != 0)
{
MainWindowViewModel.StartupData = desktop.Args[0];
Log.Information("Setting startup data from start args {Data}", desktop.Args[0]);
}
// arguments passed successfully to another application instance, exit
Environment.Exit(0);
return;
}

desktop.MainWindow = mainWindow;
AtomexApp.Start();
TemplateService = new TemplateService();
Clipboard = AvaloniaLocator.Current.GetService<IClipboard>();

var currenciesProvider = new CurrenciesProvider(CurrenciesConfigurationString);
var symbolsProvider = new SymbolsProvider(SymbolsConfiguration);

var bitfinexQuotesProvider = new BitfinexQuotesProvider(
currencies: currenciesProvider
.GetCurrencies(Network.MainNet)
.GetOrderedPreset()
.Select(c => c.Name),
baseCurrency: QuotesProvider.Usd,
log: LoggerFactory.CreateLogger<BitfinexQuotesProvider>());

var tezToolsQuotesProvider = new TezToolsQuotesProvider(
log: LoggerFactory.CreateLogger<TezToolsQuotesProvider>());

var quotesProvider = new MultiSourceQuotesProvider(
log: LoggerFactory.CreateLogger<MultiSourceQuotesProvider>(),
bitfinexQuotesProvider,
tezToolsQuotesProvider);

// init Atomex client app
AtomexApp = new AtomexApp(logger: LoggerFactory.CreateLogger("AtomexApp"))
.UseCurrenciesProvider(currenciesProvider)
.UseSymbolsProvider(symbolsProvider)
.UseCurrenciesUpdater(new CurrenciesUpdater(currenciesProvider))
.UseSymbolsUpdater(new SymbolsUpdater(symbolsProvider))
.UseQuotesProvider(quotesProvider);

var mainWindow = new MainWindow();
DialogService = new DialogService();

NotificationsService = new NotificationsService(AtomexApp, mainWindow.NotificationManager);
MainWindowViewModel = new MainWindowViewModel(AtomexApp, mainWindow);
mainWindow.DataContext = MainWindowViewModel;
desktop.Exit += OnExit;

if (desktop.Args.Length != 0)
{
MainWindowViewModel.StartupData = desktop.Args[0];
Log.Information("Setting startup data from start args {Data}", desktop.Args[0]);
}

Task.Run(async () =>
{
var ipPoint = new IPEndPoint(IPAddress.Loopback, atomexTcpPort);
using var serverSocket = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
serverSocket.Bind(ipPoint);
serverSocket.Listen();

while (true)
{
var clientSocket = await serverSocket.AcceptAsync();
var buffer = new List<byte>();

do
{
var currByte = new byte[1];
var byteCounter = clientSocket.Receive(currByte, currByte.Length, SocketFlags.None);
if (byteCounter.Equals(1))
buffer.Add(currByte[0]);
} while (clientSocket.Available != 0);

var receivedSocketText = Encoding.UTF8.GetString(buffer.ToArray(), 0, buffer.Count);
if (receivedSocketText != string.Empty)
{
MainWindowViewModel.StartupData = receivedSocketText;
Log.Information("Received startup data from socket {Data}", receivedSocketText);
_ = Dispatcher.UIThread.InvokeAsync(() => { desktop.MainWindow.Activate(); });
}

clientSocket.Disconnect(false);
clientSocket.Close();
clientSocket.Dispose();
}
});
desktop.MainWindow = mainWindow;
AtomexApp.Start();

// var sink = new InMemorySink(mainWindowViewModel.LogEvent);
// Log.Logger = new LoggerConfiguration()
// .WriteTo.Sink(sink)
// .CreateLogger();
}
_singleInstanceLoopbackService = new SingleInstanceLoopbackService();
_singleInstanceLoopbackService.RunInBackground((receivedText) =>
{
MainWindowViewModel.StartupData = receivedText;
Log.Information("Received startup data from socket {Data}", receivedText);
_ = Dispatcher.UIThread.InvokeAsync(() => { desktop.MainWindow.Activate(); });
});

base.OnFrameworkInitializationCompleted();
}

void OnExit(object sender, ControlledApplicationLifetimeExitEventArgs e)
void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs e)
{
Log.Information("Application shutdown");

try
{
AtomexApp.Stop();
Expand All @@ -219,7 +162,7 @@ void OnExit(object sender, ControlledApplicationLifetimeExitEventArgs e)

private static Assembly CoreAssembly { get; } = AppDomain.CurrentDomain
.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "Atomex.Client.Core");
.FirstOrDefault(a => a.GetName().Name == "Atomex.Client.Core") ?? throw new Exception("Can't find core library assembly");

private static string CurrenciesConfigurationString
{
Expand Down Expand Up @@ -304,31 +247,4 @@ private void ConfigureLoggers()
.AddSerilog();
}
}

class InMemorySink : ILogEventSink
{
private readonly Action<string> _logAction;

public InMemorySink(Action<string> logAction)
{
_logAction = logAction;
}

readonly ITextFormatter _textFormatter = new MessageTemplateTextFormatter("[{Level}] {Message}{Exception}");

public ConcurrentQueue<string> Events { get; } = new ConcurrentQueue<string>();

public void Emit(LogEvent logEvent)
{
if (logEvent == null)
throw new ArgumentNullException(nameof(logEvent));

var renderSpace = new StringWriter();
_textFormatter.Format(logEvent, renderSpace);

Events.Enqueue(renderSpace.ToString());

_logAction.Invoke(renderSpace.ToString());
}
}
}
2 changes: 1 addition & 1 deletion Atomex.Client.Desktop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<UseAppHost>true</UseAppHost>
<ApplicationIcon>Resources/Images/atomex_logo_256x256_ico.ico</ApplicationIcon>

<AssemblyVersion>1.3.4</AssemblyVersion>
<AssemblyVersion>1.3.5</AssemblyVersion>
<Version>1.3.5</Version>
</PropertyGroup>
<Choose>
Expand Down
68 changes: 68 additions & 0 deletions Common/CallerEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Diagnostics;

using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;
using Serilog;
using Serilog.Extensions.Logging;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace Atomex.Client.Desktop.Common
{
public class CallerEnricher : ILogEventEnricher
{
private const string CallerPropertyName = "Caller";
private const string ClassPropertyName = "Class";

public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var skip = 3;

while (true)
{
var stack = new StackFrame(skip);

if (!stack.HasMethod())
{
logEvent.AddPropertyIfAbsent(new LogEventProperty(CallerPropertyName, new ScalarValue("<unknown method>")));
logEvent.AddPropertyIfAbsent(new LogEventProperty(ClassPropertyName, new ScalarValue("<unknown class>")));
return;
}

var method = stack.GetMethod();

if (method?.DeclaringType != null &&
method.DeclaringType.Assembly != typeof(Log).Assembly && // skip "Serilog" assembly
method.DeclaringType.Assembly != typeof(SerilogLoggerProvider).Assembly && // skip "Serilog.Extensions.Logging" assembly
method.DeclaringType.Assembly != typeof(LoggerFactory).Assembly && // skip "Microsoft.Extensions.Logging" assembly
method.DeclaringType.Assembly != typeof(ILogger).Assembly) // skip "Microsoft.Extensions.Logging.Abstractions" assembly
{
var methodName = method.Name;
var className = method.DeclaringType.FullName;

var shortClassName = method.DeclaringType.DeclaringType != null
? method.DeclaringType.DeclaringType.Name
: method.DeclaringType.Name;

var caller = $"{className}.{methodName}";

logEvent.AddPropertyIfAbsent(new LogEventProperty(CallerPropertyName, new ScalarValue(caller)));
logEvent.AddPropertyIfAbsent(new LogEventProperty(ClassPropertyName, new ScalarValue(shortClassName)));

return;
}

skip++;
}
}
}

public static class LoggerCallerEnrichmentConfiguration
{
public static LoggerConfiguration WithCaller(this LoggerEnrichmentConfiguration enrichmentConfiguration)
{
return enrichmentConfiguration.With<CallerEnricher>();
}
}
}
Loading

0 comments on commit bc47fcd

Please sign in to comment.