aboutsummaryrefslogtreecommitdiff
path: root/apps/VNLib.WebServer/src/Entry.cs
blob: 1ce660d96b42a0ad69c2b1c7901b37eabd7386f7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
/*
* Copyright (c) 2024 Vaughn Nugent
* 
* Library: VNLib
* Package: VNLib.WebServer
* File: Entry.cs 
*
* Entry.cs is part of VNLib.WebServer which is part of the larger 
* VNLib collection of libraries and utilities.
*
* VNLib.WebServer is free software: you can redistribute it and/or modify 
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 2 of the License,
* or (at your option) any later version.
*
* VNLib.WebServer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License 
* along with VNLib.WebServer. If not, see http://www.gnu.org/licenses/.
*/

using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Net.Sockets;
using System.Runtime.ExceptionServices;

using VNLib.Utils.IO;
using VNLib.Utils.Memory;
using VNLib.Utils.Logging;
using VNLib.Hashing;
using VNLib.Hashing.Native.MonoCypher;

using VNLib.WebServer.Config;
using VNLib.WebServer.Bootstrap;
using VNLib.WebServer.RuntimeLoading;

namespace VNLib.WebServer
{

    static class Entry
    {
        const string STARTUP_MESSAGE =
@"VNLib.Webserver - runtime host Copyright (C) Vaughn Nugent
This program comes with ABSOLUTELY NO WARRANTY.
Licensing for this software and other libraries can be found at https://www.vaughnnugent.com/resources/software
Starting...
";

        private static readonly DirectoryInfo EXE_DIR = new(Environment.CurrentDirectory);

        private const string DEFAULT_CONFIG_PATH = "config.json";
        internal const string TCP_CONF_PROP_NAME = "tcp";
        internal const string LOAD_DEFAULT_HOSTNAME_VALUE = "[system]";
        internal const string PLUGINS_CONFIG_PROP_NAME = "plugins";


        static int Main(string[] args)
        {
            ProcessArguments procArgs = new(args);

            //Print the help menu
            if (args.Length == 0 || procArgs.HasArgument("-h") || procArgs.HasArgument("--help"))
            {
                PrintHelpMenu();
                return 0;
            }

            Console.WriteLine(STARTUP_MESSAGE);

            //Init log config builder
            ServerLogBuilder logBuilder = new();
            logBuilder.BuildForConsole(procArgs);

            //try to load the json configuration file
            IServerConfig? config = LoadConfig(procArgs);
            if (config is null)
            {
                logBuilder.AppLogConfig.CreateLogger().Error("No configuration file was found");
                return -1;
            }

            //Build logs from config
            logBuilder.BuildFromConfig(config.GetDocumentRoot());

            //Create the logger
            using ServerLogger logger = logBuilder.GetLogger();

            //Dump config to console
            if (procArgs.HasArgument("--dump-config"))
            {
                DumpConfig(config.GetDocumentRoot(), logger);
            }

            //Setup the app-domain listener
            InitAppDomainListener(procArgs, logger.AppLog);

#if !DEBUG
            if (procArgs.LogHttp)
            {
                logger.AppLog.Warn("HTTP Logging is only enabled in builds compiled with DEBUG symbols");
            }
#endif

            if (procArgs.ZeroAllocations && !MemoryUtil.Shared.CreationFlags.HasFlag(HeapCreation.GlobalZero))
            {
                logger.AppLog.Debug("Zero allocation flag was set, but the shared heap was not created with the GlobalZero flag, consider enabling zero allocations globally");
            }

            using WebserverBase server = GetWebserver(logger, config, procArgs);

            try
            {
                logger.AppLog.Information("Building service stack, populating service domain...");

                server.Configure();
            }
            catch (ServerConfigurationException sce) when (sce.InnerException is not null)
            {
                logger.AppLog.Fatal("Failed to configure server. Reason: {sce}", sce.InnerException.Message);
                return -1;
            }
            catch (ServerConfigurationException sce)
            {
                logger.AppLog.Fatal("Failed to configure server. Reason: {sce}", sce.Message);
                return -1;
            }
            catch (Exception ex) when (ex.InnerException is ServerConfigurationException sce)
            {
                logger.AppLog.Fatal("Failed to configure server. Reason: {sce}", sce.Message);
                return -1;
            }
            catch (Exception ex)
            {
                logger.AppLog.Fatal(ex, "Failed to configure server");
                return -1;
            }

            logger.AppLog.Verbose("Server configuration stage complete");

            using ManualResetEvent ShutdownEvent = new(false);

            try
            {
                logger.AppLog.Information("Starting services...");

                server.Start();

                logger.AppLog.Information("Service stack started, servers are listening.");

                //Register console cancel to cause cleanup
                Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) =>
                {
                    e.Cancel = true;
                    ShutdownEvent.Set();
                };

                /*
                 * Optional background thread to listen for commands on stdin which 
                 * can also request a server shutdown. 
                 * 
                 * The loop runs in a background thread and will not block the main thread
                 * The loop can request a server shutdown by setting the shutdown event
                 */

                if (!procArgs.HasArgument("--input-off"))
                {
                    CommandListener cmdLoop = new(ShutdownEvent, server, logger.AppLog);

                    Thread consoleListener = new(() => cmdLoop.ListenForCommands(Console.In, Console.Out, name: "stdin"))
                    {
                        IsBackground = true
                    };

                    consoleListener.Start();
                }

                logger.AppLog.Information("Main thread waiting for exit signal, press ctrl + c to exit");

                //Wait for user signal to exit
                ShutdownEvent.WaitOne();

                logger.AppLog.Information("Stopping service stack");

                server.Stop();

                //Wait for all plugins to unload and cleanup (temporary)
                Thread.Sleep(500);

                return 0;
            }
            catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse)
            {
                logger.AppLog.Fatal("Failed to start servers, address already in use");
                return (int)se.SocketErrorCode;
            }
            catch (SocketException se)
            {
                logger.AppLog.Fatal(se, "Failed to start servers due to a socket exception");
                return (int)se.SocketErrorCode;
            }
            catch (Exception ex)
            {
                logger.AppLog.Fatal(ex, "Failed to start web servers");
            }

            return -1;
        }

        static void PrintHelpMenu()
        {
            const string TEMPLATE =
@$"
    VNLib.Webserver Copyright (C) 2024 Vaughn Nugent

    A high-performance, cross-platform, single process, reference webserver built on the .NET 8.0 Core runtime.

    Option flags:
        --config         <path>     - Specifies the path to the configuration file (relative or absolute)
        --input-off                 - Disables the STDIN listener, no runtime commands will be processed
        --inline-scheduler          - Enables inline scheduling for TCP transport IO processing (not available when using TLS)
        --no-plugins                - Disables loading of dynamic plugins
        --log-http                  - Enables logging of HTTP request and response headers to the system logger (debug builds only)
        --log-transport             - Enables logging of transport events to the system logger (debug builds only)
        --dump-config               - Dumps the JSON configuration to the console during loading
        --compression-off           - Disables dynamic response compression
        --zero-alloc                - Forces all http/tcp memory pool allocations to be zeroed before use (reduced performance)
        --sequential-load           - Loads all plugins sequentially (default is concurrently)
        --no-reuse-socket           - Disables socket reuse for TCP connections (Windows only)
        --reuse-address             - Enables address reuse for TCP connections
        -h, --help                  - Prints this help menu
        -t, --threads    <num>      - Specifies the number of socket accept threads. Defaults to processor count
        -s, --silent                - Disables all console logging
        -v, --verbose               - Enables verbose logging
        -d, --debug                 - Enables debug logging for the process and all plugins
        -vv                         - Enables very verbose logging (attaches listeners for app-domain events and logs them to the output)

    Your configuration file must be a JSON or YAML encoded file and be readable to the process. You may consider keeping it in a safe 
    location outside the application and only readable to this process.

    You should disable hot-reload for production environments, for security and performance reasons.

    You may consider using the --input-off flag to disable STDIN listening for production environments for security reasons.

    Optional environment variables:
        {MemoryUtil.SHARED_HEAP_FILE_PATH} - Specifies the path to the native heap allocator library
        {MemoryUtil.SHARED_HEAP_ENABLE_DIAGNOISTICS_ENV} - Enables heap diagnostics for the shared heap 1 = enabled, 0 = disabled
        {MemoryUtil.SHARED_HEAP_GLOBAL_ZERO} - Enables zeroing of all allocations from the shared heap 1 = enabled, 0 = disabled
        {MemoryUtil.SHARED_HEAP_RAW_FLAGS} - Raw flags to pass to the shared heap allocator's HeapCreate function, hexadeciaml encoded
        {VnArgon2.ARGON2_LIB_ENVIRONMENT_VAR_NAME} - Specifies the path to the Argon2 native library
        {MonoCypherLibrary.MONOCYPHER_LIB_ENVIRONMENT_VAR_NAME} - Specifies the path to the Monocypher native library

    Usage:
        VNLib.Webserver --config <path> ... (other options)     #Starts the server from the configuration (basic usage)

";
            Console.WriteLine(TEMPLATE);
        }

        #region config

        /// <summary>
        /// Initializes the configuration DOM from the specified cmd args 
        /// or the default configuration path
        /// </summary>
        /// <param name="args">The command-line-arguments</param>
        /// <returns>A new <see cref="JsonDocument"/> that contains the application configuration</returns>
        private static IServerConfig? LoadConfig(ProcessArguments args)
        {
            //Get the config path or default config
            string configPath = args.GetArgument("--config") ?? Path.Combine(EXE_DIR.FullName, DEFAULT_CONFIG_PATH);

            return JsonServerConfig.FromFile(configPath);
        }

        private static WebserverBase GetWebserver(ServerLogger logger, IServerConfig config, ProcessArguments procArgs)
        {
            logger.AppLog.Information("Configuring production webserver");
            return new ReleaseWebserver(logger, config, procArgs);
        }

        private static void DumpConfig(JsonElement doc, ServerLogger logger)
        {
            //Dump the config to the console
            using VnMemoryStream ms = new();
            using (Utf8JsonWriter writer = new(ms, new() { Indented = true }))
            {
                doc.WriteTo(writer);
            }

            string json = Encoding.UTF8.GetString(ms.AsSpan());
            logger.AppLog.Information("Dumping configuration to console...\n{c}", json);
        }

        #endregion

        private static void InitAppDomainListener(ProcessArguments args, ILogProvider log)
        {
            AppDomain currentDomain = AppDomain.CurrentDomain;
            currentDomain.UnhandledException += delegate (object sender, UnhandledExceptionEventArgs e)
            {
                log.Fatal("UNHANDLED APPDOMAIN EXCEPTION \n {e}", e);
            };
            //If double verbose is specified, log app-domain messages
            if (args.DoubleVerbose)
            {
                log.Verbose("Double verbose mode enabled, registering app-domain listeners");

                currentDomain.FirstChanceException += delegate (object? sender, FirstChanceExceptionEventArgs e)
                {
                    log.Verbose(e.Exception, "Exception occured in app-domain ");
                };
                currentDomain.AssemblyLoad += delegate (object? sender, AssemblyLoadEventArgs args)
                {
                    log.Verbose(
                        "Assembly loaded {asm} to appdomain {domain} from\n{location}",
                        args.LoadedAssembly.FullName,
                        currentDomain.FriendlyName,
                        args.LoadedAssembly.Location
                    );
                };
                currentDomain.DomainUnload += delegate (object? sender, EventArgs e)
                {
                    log.Verbose("Domain {domain} unloaded", currentDomain.FriendlyName);
                };
            }
        }

    }
}