diff --git a/Kyoo.WindowsHost/Kyoo.WindowsHost.csproj b/Kyoo.WindowsHost/Kyoo.WindowsHost.csproj new file mode 100644 index 00000000..fe167974 --- /dev/null +++ b/Kyoo.WindowsHost/Kyoo.WindowsHost.csproj @@ -0,0 +1,22 @@ + + + + WinExe + net5.0-windows + enable + true + Kyoo.Host.Windows + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/Kyoo.WindowsHost/Program.cs b/Kyoo.WindowsHost/Program.cs new file mode 100644 index 00000000..ace92e24 --- /dev/null +++ b/Kyoo.WindowsHost/Program.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Autofac; +using Microsoft.Extensions.Hosting; + +namespace Kyoo.Host.Windows +{ + public static class Program + { + /// + /// The main entry point for the application that overrides the default host (). + /// 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. + /// + public static async Task Main(string[] args) + { + Kyoo.Program.SetupDataDir(args); + + IHost host = Kyoo.Program.CreateWebHostBuilder(args) + .ConfigureContainer(builder => + { + builder.RegisterType().As().SingleInstance(); + }) + .Build(); + + await Kyoo.Program.StartWithHost(host); + } + } +} \ No newline at end of file diff --git a/Kyoo.WindowsHost/SystemTrait.cs b/Kyoo.WindowsHost/SystemTrait.cs new file mode 100644 index 00000000..22009ab1 --- /dev/null +++ b/Kyoo.WindowsHost/SystemTrait.cs @@ -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 +{ + /// + /// A singleton that add an notification icon on the window's toolbar. + /// + public sealed class SystemTrait : IStartable, IDisposable + { + /// + /// The options containing the . + /// + private readonly IOptions _options; + + /// + /// The thread where the trait is running. + /// + private Thread? _thread; + + + /// + /// Create a new . + /// + /// The options to use. + public SystemTrait(IOptions options) + { + _options = options; + } + + /// + public void Start() + { + _thread = new Thread(() => InternalSystemTrait.Run(_options)) + { + IsBackground = true + }; + _thread.Start(); + } + + /// + 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; + } + + /// + /// The internal class for . It should be invoked via + /// . + /// + private class InternalSystemTrait : ApplicationContext + { + /// + /// The options containing the . + /// + private readonly IOptions _options; + + /// + /// The Icon that is displayed in the window's bar. + /// + private readonly NotifyIcon _icon; + + /// + /// Create a new . Used only by . + /// + /// The option containing the public url. + private InternalSystemTrait(IOptions 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); }) + }); + } + + /// + /// Run the trait in the current thread, this method does not return while the trait is running. + /// + /// The options to pass to . + public static void Run(IOptions options) + { + using InternalSystemTrait trait = new(options); + Application.Run(trait); + } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _icon.Visible = false; + _icon.Dispose(); + } + + /// + /// Open kyoo's page in the user's default browser. + /// + private void _StartBrowser() + { + Process browser = new() + { + StartInfo = new ProcessStartInfo(_options.Value.PublicUrl.ToString()) + { + UseShellExecute = true + } + }; + browser.Start(); + } + } + } +} \ No newline at end of file diff --git a/Kyoo.WindowsHost/kyoo.ico b/Kyoo.WindowsHost/kyoo.ico new file mode 100644 index 00000000..61e16024 Binary files /dev/null and b/Kyoo.WindowsHost/kyoo.ico differ diff --git a/Kyoo.sln b/Kyoo.sln index 701d760b..6ab7d6d9 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "tests\Kyoo.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.WebApp", "Kyoo.WebApp\Kyoo.WebApp.csproj", "{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.WindowsHost", "Kyoo.WindowsHost\Kyoo.WindowsHost.csproj", "{98851001-40DD-46A6-94B3-2F8D90722076}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.Release|Any CPU.ActiveCfg = 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 EndGlobal diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index b6445b3d..2d1e35e8 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -70,10 +70,11 @@ namespace Kyoo builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().InstancePerLifetimeScope(); - builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().SingleInstance(); @@ -135,8 +136,6 @@ namespace Kyoo }); services.AddHttpClient(); - - services.AddHostedService(x => x.GetService() as TaskManager); } /// diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 28ac7ff0..d4a573f9 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -44,7 +44,8 @@ - + diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index e9f21a0b..f905b8ba 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using SEnvironment = System.Environment; namespace Kyoo { @@ -28,20 +29,23 @@ namespace Kyoo #else private const string Environment = "Production"; #endif - + /// /// Main function of the program /// /// Command line arguments - public static async Task Main(string[] args) + public static Task Main(string[] args) + { + SetupDataDir(args); + return StartWithHost(CreateWebHostBuilder(args).Build()); + } + + /// + /// Start the given host and log failing exceptions. + /// + /// The host to start. + 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 { host.Services.GetRequiredService>() @@ -65,6 +69,7 @@ namespace Kyoo return builder.SetBasePath(System.Environment.CurrentDirectory) .AddJsonFile(JsonConfigPath, false, true) .AddEnvironmentVariables() + .AddEnvironmentVariables("KYOO_") .AddCommandLine(args); } @@ -73,7 +78,7 @@ namespace Kyoo /// /// The host context that contains the configuration /// The logger builder to configure. - private static void _ConfigureLogging(HostBuilderContext context, ILoggingBuilder builder) + public static void ConfigureLogging(HostBuilderContext context, ILoggingBuilder builder) { builder.AddConfiguration(context.Configuration.GetSection("logging")) .AddSimpleConsole(x => @@ -89,18 +94,19 @@ namespace Kyoo /// /// Command line parameters that can be handled by kestrel /// - /// An action to configure the logging. If it is null, will be used. + /// An action to configure the logging. If it is null, will be used. /// /// A new web host instance public static IHostBuilder CreateWebHostBuilder(string[] args, Action loggingConfiguration = null) { IConfiguration configuration = SetupConfig(new ConfigurationBuilder(), args).Build(); - loggingConfiguration ??= _ConfigureLogging; + loggingConfiguration ??= ConfigureLogging; return new HostBuilder() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) + .UseEnvironment(Environment) .ConfigureAppConfiguration(x => SetupConfig(x, args)) .ConfigureLogging(loggingConfiguration) .ConfigureServices(x => x.AddRouting()) @@ -113,6 +119,32 @@ namespace Kyoo ); } + /// + /// 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. + /// + /// The command line arguments + public static void SetupDataDir(string[] args) + { + IConfiguration parsed = new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddEnvironmentVariables("KYOO_") + .AddCommandLine(args) + .Build(); + + string path = parsed.GetValue("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); + } + /// /// An useless class only used to have a logger in the main. /// diff --git a/deployment/kyoo-windows.iss b/deployment/kyoo-windows.iss new file mode 100644 index 00000000..99d9d426 --- /dev/null +++ b/deployment/kyoo-windows.iss @@ -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 \ No newline at end of file