WindowsTrait: Creating a custom host for windows with a notification icon. Starting to create a inno setup installer

This commit is contained in:
Zoe Roux 2021-08-20 19:33:03 +02:00
parent 0187569748
commit 42469bfa67
9 changed files with 282 additions and 16 deletions

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<RootNamespace>Kyoo.Host.Windows</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Kyoo/Kyoo.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="kyoo.ico" />
<Content Include="kyoo.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using System.Threading.Tasks;
using Autofac;
using Microsoft.Extensions.Hosting;
namespace Kyoo.Host.Windows
{
public static class Program
{
/// <summary>
/// The main entry point for the application that overrides the default host (<see cref="Kyoo.Program"/>).
/// It adds a system trait for windows and since the host is build as a windows executable instead of a console
/// app, the console is not showed.
/// </summary>
public static async Task Main(string[] args)
{
Kyoo.Program.SetupDataDir(args);
IHost host = Kyoo.Program.CreateWebHostBuilder(args)
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterType<SystemTrait>().As<IStartable>().SingleInstance();
})
.Build();
await Kyoo.Program.StartWithHost(host);
}
}
}

View File

@ -0,0 +1,135 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Windows.Forms;
using Autofac;
using Kyoo.Models.Options;
using Microsoft.Extensions.Options;
namespace Kyoo.Host.Windows
{
/// <summary>
/// A singleton that add an notification icon on the window's toolbar.
/// </summary>
public sealed class SystemTrait : IStartable, IDisposable
{
/// <summary>
/// The options containing the <see cref="BasicOptions.PublicUrl"/>.
/// </summary>
private readonly IOptions<BasicOptions> _options;
/// <summary>
/// The thread where the trait is running.
/// </summary>
private Thread? _thread;
/// <summary>
/// Create a new <see cref="SystemTrait"/>.
/// </summary>
/// <param name="options">The options to use.</param>
public SystemTrait(IOptions<BasicOptions> options)
{
_options = options;
}
/// <inheritdoc />
public void Start()
{
_thread = new Thread(() => InternalSystemTrait.Run(_options))
{
IsBackground = true
};
_thread.Start();
}
/// <inheritdoc />
public void Dispose()
{
// TODO not sure that the trait is ended and that it does shutdown but the only way to shutdown the
// app anyway is via the Trait's Exit or a Signal so it's fine.
_thread?.Join();
_thread = null;
}
/// <summary>
/// The internal class for <see cref="SystemTrait"/>. It should be invoked via
/// <see cref="InternalSystemTrait.Run"/>.
/// </summary>
private class InternalSystemTrait : ApplicationContext
{
/// <summary>
/// The options containing the <see cref="BasicOptions.PublicUrl"/>.
/// </summary>
private readonly IOptions<BasicOptions> _options;
/// <summary>
/// The Icon that is displayed in the window's bar.
/// </summary>
private readonly NotifyIcon _icon;
/// <summary>
/// Create a new <see cref="InternalSystemTrait"/>. Used only by <see cref="Run"/>.
/// </summary>
/// <param name="options">The option containing the public url.</param>
private InternalSystemTrait(IOptions<BasicOptions> options)
{
_options = options;
Application.ApplicationExit += (_, _) => Dispose();
_icon = new NotifyIcon();
_icon.Text = "Kyoo";
_icon.Icon = new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "kyoo.ico"));
_icon.Visible = true;
_icon.MouseClick += (_, e) =>
{
if (e.Button != MouseButtons.Left)
return;
_StartBrowser();
};
_icon.ContextMenuStrip = new ContextMenuStrip();
_icon.ContextMenuStrip.Items.AddRange(new ToolStripItem[]
{
new ToolStripMenuItem("Exit", null, (_, _) => { Environment.Exit(0); })
});
}
/// <summary>
/// Run the trait in the current thread, this method does not return while the trait is running.
/// </summary>
/// <param name="options">The options to pass to <see cref="InternalSystemTrait"/>.</param>
public static void Run(IOptions<BasicOptions> options)
{
using InternalSystemTrait trait = new(options);
Application.Run(trait);
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_icon.Visible = false;
_icon.Dispose();
}
/// <summary>
/// Open kyoo's page in the user's default browser.
/// </summary>
private void _StartBrowser()
{
Process browser = new()
{
StartInfo = new ProcessStartInfo(_options.Value.PublicUrl.ToString())
{
UseShellExecute = true
}
};
browser.Start();
}
}
}
}

BIN
Kyoo.WindowsHost/kyoo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "tests\Kyoo.Te
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.WebApp", "Kyoo.WebApp\Kyoo.WebApp.csproj", "{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.WebApp", "Kyoo.WebApp\Kyoo.WebApp.csproj", "{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.WindowsHost", "Kyoo.WindowsHost\Kyoo.WindowsHost.csproj", "{98851001-40DD-46A6-94B3-2F8D90722076}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -65,5 +67,13 @@ Global
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Debug|Any CPU.Build.0 = Debug|Any CPU {2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Release|Any CPU.ActiveCfg = Release|Any CPU {2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Release|Any CPU.Build.0 = Release|Any CPU {2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Release|Any CPU.Build.0 = Release|Any CPU
{4FF1ECD9-6EEF-4440-B037-A661D78FB04D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FF1ECD9-6EEF-4440-B037-A661D78FB04D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FF1ECD9-6EEF-4440-B037-A661D78FB04D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FF1ECD9-6EEF-4440-B037-A661D78FB04D}.Release|Any CPU.Build.0 = Release|Any CPU
{98851001-40DD-46A6-94B3-2F8D90722076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98851001-40DD-46A6-94B3-2F8D90722076}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98851001-40DD-46A6-94B3-2F8D90722076}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98851001-40DD-46A6-94B3-2F8D90722076}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -70,10 +70,11 @@ namespace Kyoo
builder.RegisterType<LocalFileSystem>().As<IFileSystem>().SingleInstance(); builder.RegisterType<LocalFileSystem>().As<IFileSystem>().SingleInstance();
builder.RegisterType<HttpFileSystem>().As<IFileSystem>().SingleInstance(); builder.RegisterType<HttpFileSystem>().As<IFileSystem>().SingleInstance();
builder.RegisterType<TaskManager>().As<ITaskManager>().As<IHostedService>().SingleInstance();
builder.RegisterType<ConfigurationManager>().As<IConfigurationManager>().SingleInstance(); builder.RegisterType<ConfigurationManager>().As<IConfigurationManager>().SingleInstance();
builder.RegisterType<Transcoder>().As<ITranscoder>().SingleInstance(); builder.RegisterType<Transcoder>().As<ITranscoder>().SingleInstance();
builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope(); builder.RegisterType<ThumbnailsManager>().As<IThumbnailsManager>().InstancePerLifetimeScope();
builder.RegisterType<TaskManager>().As<ITaskManager>().SingleInstance();
builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope(); builder.RegisterType<LibraryManager>().As<ILibraryManager>().InstancePerLifetimeScope();
builder.RegisterType<RegexIdentifier>().As<IIdentifier>().SingleInstance(); builder.RegisterType<RegexIdentifier>().As<IIdentifier>().SingleInstance();
@ -135,8 +136,6 @@ namespace Kyoo
}); });
services.AddHttpClient(); services.AddHttpClient();
services.AddHostedService(x => x.GetService<ITaskManager>() as TaskManager);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -44,7 +44,8 @@
<ProjectReference Include="../Kyoo.WebApp/Kyoo.WebApp.csproj" /> <ProjectReference Include="../Kyoo.WebApp/Kyoo.WebApp.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="BuildTranscoder" BeforeTargets="BeforeBuild" Condition="'$(SkipTranscoder)' != 'true'"> <Target Name="BuildTranscoder" BeforeTargets="BeforeBuild"
Condition="'$(SkipTranscoder)' != 'true' And !Exists('$(TranscoderRoot)/build/$(TranscoderBinary)')">
<Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' != 'true'" Command="mkdir -p build %26%26 cd build %26%26 cmake .. %26%26 make -j" /> <Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' != 'true'" Command="mkdir -p build %26%26 cd build %26%26 cmake .. %26%26 make -j" />
<Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' == 'true'" Command="(if not exist build mkdir build) %26%26 cd build %26%26 cmake .. -G &quot;NMake Makefiles&quot; %26%26 nmake" /> <Exec WorkingDirectory="$(TranscoderRoot)" Condition="'$(IsWindows)' == 'true'" Command="(if not exist build mkdir build) %26%26 cd build %26%26 cmake .. -G &quot;NMake Makefiles&quot; %26%26 nmake" />
</Target> </Target>

View File

@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SEnvironment = System.Environment;
namespace Kyoo namespace Kyoo
{ {
@ -28,20 +29,23 @@ namespace Kyoo
#else #else
private const string Environment = "Production"; private const string Environment = "Production";
#endif #endif
/// <summary> /// <summary>
/// Main function of the program /// Main function of the program
/// </summary> /// </summary>
/// <param name="args">Command line arguments</param> /// <param name="args">Command line arguments</param>
public static async Task Main(string[] args) public static Task Main(string[] args)
{
SetupDataDir(args);
return StartWithHost(CreateWebHostBuilder(args).Build());
}
/// <summary>
/// Start the given host and log failing exceptions.
/// </summary>
/// <param name="host">The host to start.</param>
public static async Task StartWithHost(IHost host)
{ {
if (!File.Exists(JsonConfigPath))
File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, JsonConfigPath), JsonConfigPath);
IHost host = CreateWebHostBuilder(args)
.UseEnvironment(Environment)
.Build();
try try
{ {
host.Services.GetRequiredService<ILogger<Application>>() host.Services.GetRequiredService<ILogger<Application>>()
@ -65,6 +69,7 @@ namespace Kyoo
return builder.SetBasePath(System.Environment.CurrentDirectory) return builder.SetBasePath(System.Environment.CurrentDirectory)
.AddJsonFile(JsonConfigPath, false, true) .AddJsonFile(JsonConfigPath, false, true)
.AddEnvironmentVariables() .AddEnvironmentVariables()
.AddEnvironmentVariables("KYOO_")
.AddCommandLine(args); .AddCommandLine(args);
} }
@ -73,7 +78,7 @@ namespace Kyoo
/// </summary> /// </summary>
/// <param name="context">The host context that contains the configuration</param> /// <param name="context">The host context that contains the configuration</param>
/// <param name="builder">The logger builder to configure.</param> /// <param name="builder">The logger builder to configure.</param>
private static void _ConfigureLogging(HostBuilderContext context, ILoggingBuilder builder) public static void ConfigureLogging(HostBuilderContext context, ILoggingBuilder builder)
{ {
builder.AddConfiguration(context.Configuration.GetSection("logging")) builder.AddConfiguration(context.Configuration.GetSection("logging"))
.AddSimpleConsole(x => .AddSimpleConsole(x =>
@ -89,18 +94,19 @@ namespace Kyoo
/// </summary> /// </summary>
/// <param name="args">Command line parameters that can be handled by kestrel</param> /// <param name="args">Command line parameters that can be handled by kestrel</param>
/// <param name="loggingConfiguration"> /// <param name="loggingConfiguration">
/// An action to configure the logging. If it is null, <see cref="_ConfigureLogging"/> will be used. /// An action to configure the logging. If it is null, <see cref="ConfigureLogging"/> will be used.
/// </param> /// </param>
/// <returns>A new web host instance</returns> /// <returns>A new web host instance</returns>
public static IHostBuilder CreateWebHostBuilder(string[] args, public static IHostBuilder CreateWebHostBuilder(string[] args,
Action<HostBuilderContext, ILoggingBuilder> loggingConfiguration = null) Action<HostBuilderContext, ILoggingBuilder> loggingConfiguration = null)
{ {
IConfiguration configuration = SetupConfig(new ConfigurationBuilder(), args).Build(); IConfiguration configuration = SetupConfig(new ConfigurationBuilder(), args).Build();
loggingConfiguration ??= _ConfigureLogging; loggingConfiguration ??= ConfigureLogging;
return new HostBuilder() return new HostBuilder()
.UseServiceProviderFactory(new AutofacServiceProviderFactory()) .UseServiceProviderFactory(new AutofacServiceProviderFactory())
.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
.UseEnvironment(Environment)
.ConfigureAppConfiguration(x => SetupConfig(x, args)) .ConfigureAppConfiguration(x => SetupConfig(x, args))
.ConfigureLogging(loggingConfiguration) .ConfigureLogging(loggingConfiguration)
.ConfigureServices(x => x.AddRouting()) .ConfigureServices(x => x.AddRouting())
@ -113,6 +119,32 @@ namespace Kyoo
); );
} }
/// <summary>
/// Parse the data directory from environment variables and command line arguments, create it if necessary.
/// Set the current directory to said data folder and place a default configuration file if it does not already
/// exists.
/// </summary>
/// <param name="args">The command line arguments</param>
public static void SetupDataDir(string[] args)
{
IConfiguration parsed = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddEnvironmentVariables("KYOO_")
.AddCommandLine(args)
.Build();
string path = parsed.GetValue<string>("data_dir");
if (path == null)
path = Path.Combine(SEnvironment.GetFolderPath(SEnvironment.SpecialFolder.LocalApplicationData), "Kyoo");
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
SEnvironment.CurrentDirectory = path;
if (!File.Exists(JsonConfigPath))
File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, JsonConfigPath), JsonConfigPath);
}
/// <summary> /// <summary>
/// An useless class only used to have a logger in the main. /// An useless class only used to have a logger in the main.
/// </summary> /// </summary>

View File

@ -0,0 +1,39 @@
[Setup]
AppId={{31A61284-7939-46BC-B584-D2279A6EEEE8}
AppName=Kyoo
AppVersion=1.0
AppPublisher=SDG
AppPublisherURL=https://github.com/AnonymusRaccoon/Kyoo
AppSupportURL=https://github.com/AnonymusRaccoon/Kyoo
AppUpdatesURL=https://github.com/AnonymusRaccoon/Kyoo
DefaultDirName={commonpf}\Kyoo
DisableProgramGroupPage=yes
LicenseFile={#kyoo}\LICENSE
OutputBaseFilename=kyoo-windows
SetupIconFile={#kyoo}\wwwroot\favicon.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
AppCopyright=GPL-3.0
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
[Files]
Source: "{#kyoo}\Kyoo.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#kyoo}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[UninstallDelete]
Type: filesandordirs; Name: "{commonappdata}\Kyoo"
[Icons]
Name: "{autoprograms}\Kyoo"; Filename: "{app}\Kyoo.exe"
Name: "{autodesktop}\Kyoo"; Filename: "{app}\Kyoo.exe"; Tasks: desktopicon
[Run]
Filename: "{app}\Kyoo.exe"; Description: "{cm:LaunchProgram,Kyoo}"; Flags: nowait postinstall skipifsilent