Skip to content

Commit

Permalink
Merge pull request #97 from w-henderson/hot-reload
Browse files Browse the repository at this point in the history
Created Hot Reload plugin for Server
  • Loading branch information
w-henderson authored Aug 11, 2022
2 parents 126ee5f + 3b1a572 commit ff44046
Show file tree
Hide file tree
Showing 18 changed files with 611 additions and 20 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/plugins.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
on:
push:
paths:
- "plugins/**"
- ".github/**"

name: Plugins

jobs:
check:
name: Check Plugins
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2

- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true

- name: Check PHP plugin
uses: actions-rs/cargo@v1
with:
command: check
args: --manifest-path plugins/php/Cargo.toml

- name: Check Hot Reload plugin
uses: actions-rs/cargo@v1
with:
command: check
args: --manifest-path plugins/hot-reload/Cargo.toml
16 changes: 16 additions & 0 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
:: - Linux with all features (TLS and plugins)
:: - PHP plugin for Windows
:: - PHP plugin for Linux
:: - Hot Reload plugin for Windows
:: - Hot Reload plugin for Linux
::
:: Requires Rust to be installed both normally and in WSL.

Expand Down Expand Up @@ -55,6 +57,20 @@ robocopy target/release ../../dist libphp.so > nul
cd ../../dist
rename libphp.so php_plugin_linux.so

echo Building Hot Reload plugin for Windows...
cd ../plugins/hot-reload
cargo build --release -q
robocopy target/release ../../dist hot_reload.dll > nul
cd ../../dist
rename hot_reload.dll hot_reload_plugin_windows.dll

echo Building Hot Reload plugin for Linux...
cd ../plugins/hot-reload
wsl $HOME/.cargo/bin/cargo build --release -q
robocopy target/release ../../dist libhot_reload.so > nul
cd ../../dist
rename libhot_reload.so hot_reload_plugin_linux.so

cd ..

echo Build complete.
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Configuration](server/configuration.md)
- [Using PHP](server/using-php.md)
- [Using HTTPS](server/https.md)
- [Using Hot Reload](server/hot-reload.md)
- [Creating a Plugin](server/creating-a-plugin.md)
- [Humphrey WebSocket](websocket/index.md)
- [Synchronous](websocket/sync/index.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This book is up-to-date with the following crate versions.
| Crate | Version |
| ----- | ------- |
| Humphrey Core | 0.6.0 |
| Humphrey Server | 0.5.0 |
| Humphrey Server | 0.6.0 |
| Humphrey WebSocket | 0.4.0 |
| Humphrey JSON | 0.2.0 |
| Humphrey Auth | 0.1.3 |
22 changes: 22 additions & 0 deletions docs/src/server/hot-reload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Hot Reload
Humphrey supports hot reload through a first-party plugin, provided that the server was compiled with the `plugins` feature enabled and the plugin is installed.

The Hot Reload plugin is able to automatically reload webpages when the source code changes. It is not recommended for use in production, but is useful for development. It should also be noted that, when using a front-end framework such as React, the framework's built-in HMR (hot module reloading) functionality should be used instead of this plugin.

HTML pages are reloaded by requesting the updated page through a `fetch` call, then writing this to the page. This avoids the need for the page to be reloaded manually. CSS and JavaScript are reloaded by requesting the updated data, then replacing the old script or stylesheet. Images are reloaded in the same way. Other resources are currently unable to be dynamically reloaded.

When JavaScript is reloaded, the updated script will be executed upon load in the same context as the old script. This means that any `const` declarations may cause errors, but this is unavoidable as without executing the new script, none of the changes can be used. For this reason, the Hot Reload plugin is more suitable for design changes than for functionality changes.

**Warning:** Hot Reload disables caching so that changes are immediately visible.

## Configuration
In the plugins section of the configuration file, add the following:

```conf
hot-reload {
library "path/to/hot-reload.dll" # Path to the compiled library
ws_route "/ws" # Route to the WebSocket endpoint
}
```

Specifying the WebSocket route is optional. If not specified, the default is `/__hot-reload-ws` in order to avoid conflicts with other configured WebSocket endpoints.
1 change: 1 addition & 0 deletions examples/plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2018"
[dependencies]
humphrey = { path = "../../humphrey" }
humphrey_server = { path = "../../humphrey-server", features = ["plugins"] }
humphrey_ws = { path = "../../humphrey-ws" }

[lib]
crate-type = ["cdylib", "rlib"]
Expand Down
35 changes: 34 additions & 1 deletion examples/plugin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use humphrey::http::headers::HeaderType;
use humphrey::http::{Request, Response, StatusCode};
use humphrey::stream::Stream;

use humphrey_server::config::RouteConfig;
use humphrey_server::declare_plugin;
use humphrey_server::plugins::plugin::Plugin;
use humphrey_server::server::server::AppState;

use humphrey_ws::{websocket_handler, Message, WebsocketStream};

use std::sync::Arc;

#[derive(Debug, Default)]
Expand Down Expand Up @@ -41,7 +44,37 @@ impl Plugin for ExamplePlugin {
None
}

fn on_response(&self, response: &mut Response, state: Arc<AppState>) {
fn on_websocket_request(
&self,
request: &mut Request,
stream: Stream,
state: Arc<AppState>,
_: Option<&RouteConfig>,
) -> Option<Stream> {
state.logger.info(&format!(
"Example plugin read a WebSocket request from {}",
request.address
));

// If the requested resource is "/override" then override the response (which would ordinarily be closing the WebSocket connection). For this example, we'll just complete the WebSocket handshake and send a message back to the client.
if &request.uri == "/override" {
state
.logger
.info("Example plugin overrode a WebSocket connection");

websocket_handler(|mut stream: WebsocketStream, _| {
stream
.send(Message::new(b"Response overridden by example plugin :)"))
.ok();
})(request.clone(), stream, state);

return None;
}

Some(stream)
}

fn on_response(&self, response: &mut Response, state: Arc<AppState>, _: &RouteConfig) {
// Insert a header to the response
response.headers.add("X-Example-Plugin", "true");

Expand Down
2 changes: 1 addition & 1 deletion humphrey-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "humphrey_server"
version = "0.5.2"
version = "0.6.0"
edition = "2018"
license = "MIT"
homepage = "https://github.com/w-henderson/Humphrey"
Expand Down
12 changes: 11 additions & 1 deletion humphrey-server/src/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub struct HostConfig {
}

/// Represents the type of a route.
#[derive(Copy, Clone, Debug, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RouteType {
/// Serve a single file.
File,
Expand All @@ -67,6 +67,8 @@ pub enum RouteType {
Proxy,
/// Redirect clients to another server.
Redirect,
/// Proxies WebSocket requests to this route to another server.
ExclusiveWebSocket,
}

/// Represents configuration for a specific route.
Expand Down Expand Up @@ -506,6 +508,14 @@ fn parse_route(
});
} else if !conf.contains_key("websocket") {
return Err("Invalid route configuration, every route must contain either the `file`, `directory`, `proxy` or `redirect` field, unless it defines a WebSocket proxy with the `websocket` field");
} else {
routes.push(RouteConfig {
route_type: RouteType::ExclusiveWebSocket,
matches: wild.to_string(),
path: None,
load_balancer: None,
websocket_proxy,
});
}
}

Expand Down
27 changes: 25 additions & 2 deletions humphrey-server/src/plugins/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::config::RouteConfig;
use crate::plugins::plugin::{Plugin, PluginLoadResult};
use crate::server::server::AppState;
use humphrey::http::{Request, Response};
use humphrey::stream::Stream;

use libloading::Library;
use std::collections::HashMap;
Expand Down Expand Up @@ -84,10 +85,32 @@ impl PluginManager {
None
}

/// Calls the `on_websocket_request` function on every plugin.
/// If a plugin handles the stream, the function immediately returns.
pub fn on_websocket_request(
&self,
request: &mut Request,
mut stream: Stream,
state: Arc<AppState>,
route: Option<&RouteConfig>,
) -> Option<Stream> {
for plugin in &self.plugins {
if let Some(returned_stream) =
plugin.on_websocket_request(request, stream, state.clone(), route)
{
stream = returned_stream;
} else {
return None;
}
}

Some(stream)
}

/// Calls the `on_response` function on every plugin.
pub fn on_response(&self, response: &mut Response, state: Arc<AppState>) {
pub fn on_response(&self, response: &mut Response, state: Arc<AppState>, route: &RouteConfig) {
for plugin in &self.plugins {
plugin.on_response(response, state.clone());
plugin.on_response(response, state.clone(), route);
}
}

Expand Down
17 changes: 16 additions & 1 deletion humphrey-server/src/plugins/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use crate::config::RouteConfig;
use crate::server::server::AppState;
use humphrey::http::{Request, Response};
use humphrey::stream::Stream;

use std::any::Any;
use std::collections::HashMap;
Expand Down Expand Up @@ -46,9 +47,23 @@ pub trait Plugin: Any + Send + Sync + Debug {
None
}

/// Called when a WebSocket request is received but before it is processed. May modify the request in-place.
/// Unlike `on_request`, this method should return `None` if the WebSocket request is being handled by the plugin, and should return the stream back to Humphrey if the request is not being handled by the plugin.
///
/// **Important:** If the plugin returns `Some(stream)`, it must not modify or close the stream.
fn on_websocket_request(
&self,
request: &mut Request,
stream: Stream,
state: Arc<AppState>,
route: Option<&RouteConfig>,
) -> Option<Stream> {
Some(stream)
}

/// Called when a response has been generated but not yet sent.
/// May modify the response in-place.
fn on_response(&self, response: &mut Response, state: Arc<AppState>) {}
fn on_response(&self, response: &mut Response, state: Arc<AppState>, route: &RouteConfig) {}

/// Called when the plugin is about to be unloaded.
/// Any clean-up should be done here.
Expand Down
Loading

0 comments on commit ff44046

Please sign in to comment.