Skip to content

Commit 3e1d2b2

Browse files
Silvengadaveaglick
authored andcommitted
WIP: Adds basic support for LiveReload when watching/previewing (#420)
* Added basic support for LiveReload when watching + previewing. * Support for standalone LR host when watching. * Added basic coverage on LiveReloadServer. * Added support for Win7 using Fleck web sockets. * Added hostname tests. * Added script injection support. * Disabled hostname tests under CI - requires a x86 runner.
1 parent 994edee commit 3e1d2b2

24 files changed

Lines changed: 2239 additions & 24 deletions

src/clients/Wyam/Commands/BuildCommand.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.CommandLine;
5-
using System.IO;
65
using System.Linq;
76
using System.Threading;
7+
88
using Wyam.Common.IO;
9+
using Wyam.Common.Tracing;
910
using Wyam.Configuration.Preprocessing;
10-
using Trace = Wyam.Common.Tracing.Trace;
11+
using Wyam.LiveReload;
1112

1213
namespace Wyam.Commands
1314
{
@@ -18,7 +19,7 @@ internal class BuildCommand : Command
1819
private readonly InterlockedBool _exit = new InterlockedBool(false);
1920
private readonly InterlockedBool _newEngine = new InterlockedBool(false);
2021
private readonly ConfigOptions _configOptions = new ConfigOptions();
21-
22+
2223
private bool _preview = false;
2324
private int _previewPort = 5080;
2425
private DirectoryPath _previewVirtualDirectory = null;
@@ -27,7 +28,7 @@ internal class BuildCommand : Command
2728
private bool _verifyConfig = false;
2829
private DirectoryPath _previewRoot = null;
2930
private bool _watch = false;
30-
31+
3132
public override string Description => "Runs the build process (this is the default command).";
3233

3334
public override string[] SupportedDirectives => new[]
@@ -106,7 +107,7 @@ private static void AddSettings(IDictionary<string, object> settings, IReadOnlyL
106107
{
107108
foreach (KeyValuePair<string, object> kvp in MetadataParser.Parse(value))
108109
{
109-
settings[kvp.Key] = kvp.Value;
110+
settings[kvp.Key] = kvp.Value;
110111
}
111112
}
112113

@@ -119,7 +120,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor)
119120
{
120121
// Get the standard input stream
121122
_configOptions.Stdin = StandardInputReader.Read();
122-
123+
123124
// Fix the root folder and other files
124125
DirectoryPath currentDirectory = Environment.CurrentDirectory;
125126
_configOptions.RootPath = _configOptions.RootPath == null ? currentDirectory : currentDirectory.Combine(_configOptions.RootPath);
@@ -162,6 +163,15 @@ protected override ExitCode RunCommand(Preprocessor preprocessor)
162163

163164
bool messagePump = false;
164165

166+
// Start the LiveReload server.
167+
bool runLiveReloadServer = _watch;
168+
LiveReloadServer liveReloadServer = null;
169+
if (runLiveReloadServer)
170+
{
171+
liveReloadServer = new LiveReloadServer();
172+
liveReloadServer.StartStandaloneHost();
173+
}
174+
165175
// Start the preview server
166176
IDisposable previewServer = null;
167177
if (_preview)
@@ -170,7 +180,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor)
170180
DirectoryPath previewPath = _previewRoot == null
171181
? engineManager.Engine.FileSystem.GetOutputDirectory().Path
172182
: engineManager.Engine.FileSystem.GetOutputDirectory(_previewRoot).Path;
173-
previewServer = PreviewServer.Start(previewPath, _previewPort, _previewForceExtension, _previewVirtualDirectory);
183+
previewServer = PreviewServer.Start(previewPath, _previewPort, _previewForceExtension, _previewVirtualDirectory, liveReloadServer);
174184
}
175185

176186
// Start the watchers
@@ -192,7 +202,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor)
192202
{
193203
Trace.Information("Watching configuration file {0}", _configOptions.ConfigFilePath);
194204
configFileWatcher = new ActionFileSystemWatcher(engineManager.Engine.FileSystem.GetOutputDirectory().Path,
195-
new[] { _configOptions.ConfigFilePath.Directory }, false, _configOptions.ConfigFilePath.FileName.FullPath, path =>
205+
new[] {_configOptions.ConfigFilePath.Directory}, false, _configOptions.ConfigFilePath.FileName.FullPath, path =>
196206
{
197207
FilePath filePath = new FilePath(path);
198208
if (_configOptions.ConfigFilePath.Equals(filePath))
@@ -228,7 +238,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor)
228238
// Wait for activity
229239
while (true)
230240
{
231-
_messageEvent.WaitOne(); // Blocks the current thread until a signal
241+
_messageEvent.WaitOne(); // Blocks the current thread until a signal
232242
if (_exit)
233243
{
234244
break;
@@ -283,6 +293,8 @@ protected override ExitCode RunCommand(Preprocessor preprocessor)
283293
{
284294
exitCode = ExitCode.ExecutionError;
285295
}
296+
297+
liveReloadServer?.RebuildCompleted(changedFiles);
286298
}
287299
}
288300

@@ -301,6 +313,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor)
301313
inputFolderWatcher?.Dispose();
302314
configFileWatcher?.Dispose();
303315
previewServer?.Dispose();
316+
liveReloadServer?.Dispose();
304317
}
305318

306319
return exitCode;

src/clients/Wyam/Commands/PreviewCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ protected override void ParseParameters(ArgumentSyntax syntax)
3030
protected override ExitCode RunCommand(Preprocessor preprocessor)
3131
{
3232
_path = new DirectoryPath(Environment.CurrentDirectory).Combine(_path ?? "output");
33-
using (PreviewServer.Start(_path, _port, _forceExtension, _virtualDirectory))
33+
using (PreviewServer.Start(_path, _port, _forceExtension, _virtualDirectory, null))
3434
{
3535
Trace.Information("Hit any key to exit");
3636
Console.ReadKey();
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
5+
using AngleSharp;
6+
using AngleSharp.Dom;
7+
using AngleSharp.Dom.Html;
8+
using AngleSharp.Parser.Html;
9+
10+
using Microsoft.Owin;
11+
12+
namespace Wyam.LiveReload
13+
{
14+
internal class LiveReloadScriptInjectionMiddleware : OwinMiddleware
15+
{
16+
private readonly string _scriptPath;
17+
18+
internal HtmlParser HtmlParser { get; set; } = new HtmlParser();
19+
20+
public LiveReloadScriptInjectionMiddleware(OwinMiddleware next, string scriptPath) : base(next)
21+
{
22+
_scriptPath = scriptPath;
23+
}
24+
25+
public override async Task Invoke(IOwinContext context)
26+
{
27+
Stream originalBody = context.Response.Body;
28+
MemoryStream interceptedBody = new MemoryStream();
29+
context.Response.Body = interceptedBody;
30+
31+
await Next.Invoke(context);
32+
33+
if (IsHtmlDocument(context))
34+
{
35+
interceptedBody.Position = 0;
36+
IHtmlDocument document = HtmlParser.Parse(interceptedBody);
37+
38+
IElement script = document.CreateElement("script");
39+
script.SetAttribute("type", "text/javascript");
40+
script.SetAttribute("src", _scriptPath);
41+
document.Body.Append(script);
42+
43+
MemoryStream newContentBuffer = new MemoryStream();
44+
StreamWriter writer = new StreamWriter(newContentBuffer);
45+
46+
document.ToHtml(writer, new AutoSelectedMarkupFormatter());
47+
writer.Flush();
48+
49+
context.Response.ContentLength = newContentBuffer.Length;
50+
newContentBuffer.Position = 0;
51+
newContentBuffer.CopyTo(originalBody);
52+
53+
context.Response.Body = originalBody;
54+
}
55+
else
56+
{
57+
interceptedBody.Position = 0;
58+
interceptedBody.CopyTo(originalBody);
59+
60+
context.Response.Body = originalBody;
61+
}
62+
}
63+
64+
private bool IsHtmlDocument(IOwinContext context)
65+
{
66+
const string rfc2854Type = "text/html";
67+
string contentType = context.Response.ContentType;
68+
return string.Equals(contentType, rfc2854Type, StringComparison.OrdinalIgnoreCase);
69+
}
70+
}
71+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
3+
using Owin;
4+
5+
namespace Wyam.LiveReload
6+
{
7+
public static class LiveReloadScriptInjectionMiddlewareExtensions
8+
{
9+
public static IAppBuilder UseLiveReloadScriptInjections(this IAppBuilder builder, string scriptPath)
10+
{
11+
if (builder == null)
12+
{
13+
throw new ArgumentNullException(nameof(builder));
14+
}
15+
return builder.Use<LiveReloadScriptInjectionMiddleware>(scriptPath);
16+
}
17+
}
18+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Reflection;
6+
7+
using Microsoft.Owin;
8+
using Microsoft.Owin.FileSystems;
9+
using Microsoft.Owin.StaticFiles;
10+
11+
using Owin;
12+
using Owin.WebSocket.Extensions;
13+
14+
using Wyam.Common.Tracing;
15+
using Wyam.Server;
16+
17+
namespace Wyam.LiveReload
18+
{
19+
internal class LiveReloadServer : IDisposable
20+
{
21+
private readonly ConcurrentBag<IReloadClient> _clients = new ConcurrentBag<IReloadClient>();
22+
private HttpServer _server;
23+
24+
public virtual IEnumerable<IReloadClient> ReloadClients => _clients.ToArray();
25+
26+
public void StartStandaloneHost(int port = 35729, bool throwExceptions = false)
27+
{
28+
try
29+
{
30+
_server = new HttpServer();
31+
_server.StartServer(port, AddHostMiddleware);
32+
}
33+
catch (Exception ex)
34+
{
35+
Trace.Warning($"Error while running the LiveReload server: {ex.Message}");
36+
if (throwExceptions)
37+
{
38+
throw;
39+
}
40+
}
41+
42+
Trace.Verbose($"LiveReload server listening on port {port}.");
43+
}
44+
45+
public void AddInjectionMiddleware(IAppBuilder app)
46+
{
47+
// Inject LR script.
48+
app.UseLiveReloadScriptInjections("/livereload.js");
49+
}
50+
51+
public void AddHostMiddleware(IAppBuilder app)
52+
{
53+
// Host livereload.js
54+
Assembly liveReloadAssembly = typeof(LiveReloadServer).Assembly;
55+
string rootNamespace = typeof(LiveReloadServer).Namespace;
56+
IFileSystem reloadFilesystem = new EmbeddedResourceFileSystem(liveReloadAssembly, $"{rootNamespace}");
57+
app.UseStaticFiles(new StaticFileOptions
58+
{
59+
RequestPath = PathString.Empty,
60+
FileSystem = reloadFilesystem,
61+
ServeUnknownFileTypes = true
62+
});
63+
64+
// Host ws://
65+
app.MapFleckRoute<ReloadClient>("/livereload", connection => _clients.Add((ReloadClient) connection));
66+
}
67+
68+
public void RebuildCompleted(ICollection<string> filesChanged)
69+
{
70+
foreach (IReloadClient client in ReloadClients.Where(x => x.IsConnected))
71+
{
72+
foreach (string modifiedFile in filesChanged)
73+
{
74+
client.NotifyOfChanges(modifiedFile);
75+
}
76+
}
77+
}
78+
79+
public void Dispose()
80+
{
81+
_server?.Dispose();
82+
}
83+
}
84+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Wyam.LiveReload.Messages
2+
{
3+
internal class BasicMessage : ILiveReloadMessage
4+
{
5+
public string Command { get; set; }
6+
}
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Collections.Generic;
2+
3+
namespace Wyam.LiveReload.Messages
4+
{
5+
internal class HelloMessage : ILiveReloadMessage
6+
{
7+
public ICollection<string> Protocols { get; set; }
8+
9+
public string ServerName { get; set; } = "Wyam";
10+
11+
public string Command { get; set; } = "hello";
12+
}
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Wyam.LiveReload.Messages
2+
{
3+
internal interface ILiveReloadMessage
4+
{
5+
string Command { get; set; }
6+
}
7+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Wyam.LiveReload.Messages
2+
{
3+
internal class InfoMessage : ILiveReloadMessage
4+
{
5+
public string Command { get; set; } = "info";
6+
7+
public string Url { get; set; }
8+
}
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Wyam.LiveReload.Messages
2+
{
3+
internal class ReloadMessage : ILiveReloadMessage
4+
{
5+
public string Path { get; set; }
6+
7+
public bool LiveCss { get; set; }
8+
9+
public string Command { get; set; } = "reload";
10+
}
11+
}

0 commit comments

Comments
 (0)