1 /+
2     This file is part of Reloaded Vibes.
3     Copyright (c) 2019  0xEAB
4 
5     Distributed under the Boost Software License, Version 1.0.
6        (See accompanying file LICENSE_1_0.txt or copy at
7              https://www.boost.org/LICENSE_1_0.txt)
8  +/
9 module reloadedvibes.app;
10 
11 import std.algorithm : each, map;
12 import std.datetime : dur;
13 import std.file : exists, isDir;
14 import std.getopt;
15 import std.path : absolutePath, buildNormalizedPath;
16 import std.stdio : stdout, stderr;
17 
18 import vibe.core.core : runTask, sleep;
19 import vibe.core.log : LogLevel, setLogLevel;
20 import vibe.http.server : HTTPListener;
21 
22 import reloadedvibes.action;
23 import reloadedvibes.server;
24 import reloadedvibes.utils;
25 import reloadedvibes.watcher;
26 
27 enum appName = "Reloaded Vibes";
28 
29 int main(string[] args)
30 {
31     immutable argc = args.length;
32 
33     bool optPrintVersionInfo;
34     string optSocketService = "127.0.0.1:3001";
35     bool optDisableService = false;
36     string[] optWatchDirectories = [];
37     string[] optActions = [];
38     string optSocketWebServer;
39     string optDocumentRootWebServer;
40     bool optNoInjectWebServer;
41 
42     GetoptResult opt;
43 
44     try
45     {
46         // dfmt off
47         opt = getopt(
48             args,
49             config.passThrough,
50             "s|socket", "Socket to bind the notification service to", &optSocketService,
51             "w|watch", "Paths to watch", &optWatchDirectories,
52             "a|action", "Commandlines to execute before reloading", &optActions,
53             "n|noservice", "Disable the notification service\n", &optDisableService,
54 
55             "S|webserver", "<addr>:<port>  Enables the built-in webserver", &optSocketWebServer,
56             "d|htdocs", "Document root for the built-in webserver", &optDocumentRootWebServer,
57             "j|noinject", "Disables script tag injection for HTML files\n", &optNoInjectWebServer,
58 
59             "version", "Display the version of this program.", &optPrintVersionInfo,
60         );
61        // dfmt on
62     }
63     catch (Exception ex)
64     {
65         stderr.writeln(ex.msg);
66         return 1;
67     }
68 
69     // -- Help?
70     if ((argc == 1) || opt.helpWanted)
71     {
72         printHelp(args[0], opt);
73         return 0;
74     }
75     else if (optPrintVersionInfo)
76     {
77         printVersionInfo();
78         return 0;
79     }
80 
81     debug
82     {
83         setLogLevel(LogLevel.diagnostic);
84     }
85     else
86     {
87         setLogLevel(LogLevel.warn);
88     }
89 
90     Socket service;
91     Watcher watcher;
92     Socket webserver;
93     HTTPListener[] listeners;
94     void delegate()[] doInit;
95 
96     // -- Watcher
97     if (optWatchDirectories.length == 0)
98     {
99         stderr.writeln("No directory to watch specified, use --watch to pass one");
100         return 1;
101     }
102 
103     auto watchDirectories = optWatchDirectories.map!(x => x.absolutePath.buildNormalizedPath());
104 
105     watcher = new Watcher(watchDirectories);
106 
107     // -- Service
108     if (!optDisableService)
109     {
110         if (!tryParseSocket(optSocketService, service))
111         {
112             stderr.writeln("Bad service socket specified");
113             return 1;
114         }
115 
116         doInit ~= { listeners ~= registerService(service, watcher); };
117     }
118     else
119     {
120         optNoInjectWebServer = true;
121     }
122 
123     if (optActions.length > 0)
124     {
125         auto awcl = fromCommandLines(watcher, optActions);
126 
127         // Initial execution
128         // Since the action is usually some preprocessor or something,
129         // it should also get executed on application launch
130         doInit ~= {
131             stdout.writeln("\nPre-executing actions...");
132             awcl.notify();
133 
134             runTask(delegate() @trusted {
135                 while (true)
136                 {
137                     awcl.query();
138                     sleep(dur!"msecs"(200));
139                 }
140             });
141         };
142     }
143 
144     // -- Webserver
145     if (optSocketWebServer !is null)
146     {
147         if (!tryParseSocket(optSocketWebServer, webserver))
148         {
149             stderr.writeln("Bad webserver socket specified");
150             return 1;
151         }
152 
153         if (optDocumentRootWebServer is null)
154         {
155             stderr.writeln("No document root specified, use --htdocs to do so");
156             return 1;
157         }
158 
159         if (!optDocumentRootWebServer.exists || !optDocumentRootWebServer.isDir)
160         {
161             stderr.writeln("Bad document root specified");
162             return 1;
163         }
164 
165         optDocumentRootWebServer = optDocumentRootWebServer.absolutePath.buildNormalizedPath();
166 
167         if (optNoInjectWebServer)
168         {
169             doInit ~= {
170                 listeners ~= registerStaticWebserver(webserver, optDocumentRootWebServer);
171             };
172         }
173         else
174         {
175             doInit ~= {
176                 listeners ~= registerStaticWebserver(webserver, optDocumentRootWebServer, service);
177             };
178         }
179     }
180 
181     // -- Print info
182 
183     stdout.writeln(appName, "\n");
184 
185     watchDirectories.each!(dir => stdout.writeln("Watching:                ", dir));
186 
187     if (!optDisableService)
188     {
189         stdout.writeln();
190         stdout.writeln("Notification service:    http://", service.toString);
191     }
192 
193     if (optSocketWebServer !is null)
194     {
195         // dfmt off
196         stdout.writeln();
197         stdout.writeln("Built-in webserver:      http://", webserver.toString);
198         stdout.writeln("Serving:                 ", optDocumentRootWebServer);
199         stdout.writeln("Script injection:        ", ((optNoInjectWebServer) ? "disabled" : "enabled"));
200         // dfmt on
201     }
202 
203     stdout.writeln();
204     optActions.each!(act => stdout.writeln("Action:                  ", act));
205 
206     stdout.writeln();
207 
208     // -- Run
209     doInit.each!(x => x());
210     run(listeners);
211     return 0;
212 }
213 
214 void printHelp(string args0, GetoptResult opt)
215 {
216     // Ideally, this help text will not exceed a size of
217     // 80x23, so that it's fully visible on an 80x24 terminal.
218 
219     size_t getIndent()
220     {
221         immutable l = args0.length + 5;
222         return (l <= 29) ? l : 8;
223     }
224 
225     string makeIndent()
226     {
227         enum indent = "                             ";
228         return indent[0 .. getIndent()];
229     }
230 
231     immutable indent = makeIndent();
232 
233     // dfmt off
234     defaultGetoptPrinter(
235         appName ~ "\n\n  Usage:\n    " ~ args0 ~ " <options>\n\n  Example:\n    "
236             ~ args0 ~ " --socket=127.0.0.1:3001\n"
237             ~ indent ~ "--watch=./src            --watch=./sass\n"
238             ~ indent ~ "--action=\"npm run build\" --action=\"./refreshDB.sh\""
239             ~ "\n\nAvailable options:\n==================",
240         opt.options
241     );
242 }
243 
244 void printVersionInfo()
245 {
246     stdout.write(import("version.txt"));
247 }