diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bad2f04a..d52a3417 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -289,7 +289,7 @@ jobs: # Write and read notes from a file to avoid quoting breaking things echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt - gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/server/* + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/server/target/distrib/* announce: needs: diff --git a/builder/src/main.rs b/builder/src/main.rs index 8c3a35a3..4a3c115b 100644 --- a/builder/src/main.rs +++ b/builder/src/main.rs @@ -514,6 +514,9 @@ fn run_prerelease() -> io::Result<()> { // Clean out all bundled files before the rebuild. remove_dir_all_if_exists("../client/static/bundled")?; run_install(true)?; + run_cmd!( + cargo test export_bindings; + )?; run_script("npm", &["run", "dist"], "../client", true)?; Ok(()) } diff --git a/client/package-lock.json b/client/package-lock.json index 7d4c99bb..325e19a7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "codechat-editor-client", - "version": "0.1.20", + "version": "0.1.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codechat-editor-client", - "version": "0.1.20", + "version": "0.1.21", "license": "GPL-3.0-or-later", "dependencies": { "@codemirror/lang-cpp": "^6", @@ -1470,14 +1470,13 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.8.tgz", - "integrity": "sha512-Yd820OqLNN/Rpyod/2uGB+SdiFXVDfI/bylOMvtvpl0nIjRRovXIfek65CwWUewOhTi+m2jjRruPbttcbkd1VA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.4.tgz", + "integrity": "sha512-MrfbnMgfKexic6mxC4xrSBVQHSvmvhz7qtSDG5cHg4xn8kHXkPltUY44R5u4ghYf+1rXpOvC2drbMcE1rZ3a2A==", "dev": true, - "hasInstallScript": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/pkgr" @@ -3470,11 +3469,10 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.7", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.7.tgz", - "integrity": "sha512-xztdELuHs7grBM+qdMUF4M4SjPpeOMN3kx7sGU6ifl5yibck/GRa0+0d+m1lPsGNkd+2bIWh2lUUTzX7MX/obw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, - "hasInstallScript": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" @@ -3615,9 +3613,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", - "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", + "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", "dev": true, "license": "MIT", "dependencies": { @@ -5929,14 +5927,13 @@ } }, "node_modules/synckit": { - "version": "0.11.9", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.9.tgz", - "integrity": "sha512-02JE2dOputr8ku4SNqAnqhGo0FaHcvSvAuzXMLdvygH4dqu3PXyUdePefNozFcCznNtQwf6Wn98ZSLa+ArRqZQ==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.10.tgz", + "integrity": "sha512-n6Ze5AGOURWdQ9Kg/wqI1//4OBw9V1zuOTj7uQlpAjtpe2bhgPBpmSFXvapbP3KxcRoqo996J28kdT2ly4w9UA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.4" + "@pkgr/core": "^0.3.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" diff --git a/client/package.json b/client/package.json index de314199..62a55325 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "codechat-editor-client", - "version": "0.1.20", + "version": "0.1.21", "description": "The CodeChat Editor Client, part of a web-based literate programming editor (the CodeChat Editor).", "homepage": "https://github.com/bjones1/CodeChat_Editor", "type": "module", diff --git a/docs/changelog.md b/docs/changelog.md index 98495ad0..476b624d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -21,6 +21,9 @@ Changelog * [Github master](https://github.com/bjones1/CodeChat_Editor): * No changes. +* v0.1.21, 2025-Jul-18: + * Allow specifying the host address the server binds to. + * Send server logs to the console by default. * v0.1.20, 2025-Jul-18: * Correct data corruption in Client on delete/insert diff operations. * v0.1.19, 2025-Jul-17:  diff --git a/extensions/VSCode/package-lock.json b/extensions/VSCode/package-lock.json index 830903dd..fedb5a12 100644 --- a/extensions/VSCode/package-lock.json +++ b/extensions/VSCode/package-lock.json @@ -1,12 +1,12 @@ { "name": "codechat-editor-client", - "version": "0.1.20", + "version": "0.1.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codechat-editor-client", - "version": "0.1.20", + "version": "0.1.21", "license": "GPL-3.0-only", "dependencies": { "escape-html": "^1", @@ -3088,11 +3088,10 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.7", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.7.tgz", - "integrity": "sha512-xztdELuHs7grBM+qdMUF4M4SjPpeOMN3kx7sGU6ifl5yibck/GRa0+0d+m1lPsGNkd+2bIWh2lUUTzX7MX/obw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, - "hasInstallScript": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" diff --git a/extensions/VSCode/package.json b/extensions/VSCode/package.json index 97f9dd7e..e0befe64 100644 --- a/extensions/VSCode/package.json +++ b/extensions/VSCode/package.json @@ -1,6 +1,6 @@ { "name": "codechat-editor-client", - "version": "0.1.20", + "version": "0.1.21", "publisher": "CodeChat", "engines": { "vscode": "^1.61.0" diff --git a/server/Cargo.lock b/server/Cargo.lock index 8ac597c4..7bfee42d 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -570,7 +570,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "codechat-editor-server" -version = "0.1.20" +version = "0.1.21" dependencies = [ "actix-files", "actix-http", @@ -2111,9 +2111,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", diff --git a/server/Cargo.toml b/server/Cargo.toml index 1b37be55..cec09c55 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -31,7 +31,7 @@ license = "GPL-3.0-only" name = "codechat-editor-server" readme = "../README.md" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.1.20" +version = "0.1.21" # This library allows other packages to use core CodeChat Editor features. [lib] diff --git a/server/log4rs.yml b/server/log4rs.yml index a5bb18a4..39326cb8 100644 --- a/server/log4rs.yml +++ b/server/log4rs.yml @@ -26,9 +26,6 @@ appenders: kind: console encoder: pattern: "{d} {l} {t} {L} - {m}{n}" - filters: - - kind: threshold - level: warn # File appender for INFO, WARN, and ERROR levels file_appender: diff --git a/server/src/main.rs b/server/src/main.rs index 28d911a4..4565b0ba 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -22,6 +22,8 @@ use std::{ env, fs, io::{self, Read}, + net::{IpAddr, Ipv4Addr, SocketAddr}, + ops::RangeInclusive, path::PathBuf, process::{Child, Command, Stdio}, time::SystemTime, @@ -34,7 +36,7 @@ use clap::{Parser, Subcommand}; use log::LevelFilter; // ### Local -use code_chat_editor::webserver::{self, GetServerUrlError, IP_ADDRESS, path_to_url}; +use code_chat_editor::webserver::{self, GetServerUrlError, path_to_url}; // Data structures // --------------- @@ -48,8 +50,12 @@ struct Cli { #[command(subcommand)] command: Commands, - /// Select the port to use for the server. - #[arg(short, long, default_value_t = 8080)] + /// The address to serve. + #[arg(long, default_value_t = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))] + host: IpAddr, + + /// The port to use for the server. + #[arg(short, long, default_value_t = 8080, value_parser = port_in_range)] port: u16, /// Used for testing only. @@ -88,7 +94,7 @@ enum Commands { // The following code implements the command-line interface for the CodeChat // Editor. impl Cli { - fn run(self) -> Result<(), Box> { + fn run(self, addr: &SocketAddr) -> Result<(), Box> { match &self.command { Commands::Serve { log } => { #[cfg(debug_assertions)] @@ -98,15 +104,17 @@ impl Cli { return Ok(()); } webserver::configure_logger(log.unwrap_or(LevelFilter::Info))?; - webserver::main(self.port).unwrap(); + webserver::main(addr).unwrap(); } Commands::Start { open } => { // Poll the server to ensure it starts. let mut process: Option = None; let now = SystemTime::now(); + // If host is 0.0.0.0, use localhost to monitor it. + let ping_addr = fix_addr(addr); loop { // Look for a ping/pong response from the server. - match minreq::get(format!("http://{IP_ADDRESS}:{}/ping", self.port)) + match minreq::get(format!("http://{ping_addr}/ping")) .with_timeout(3) .send() { @@ -122,7 +130,7 @@ impl Cli { // -- if `open` used `$BROWSER` (following // Pyhton), it should work. if let Some(open_path) = open { - let address = get_server_url(self.port)?; + let address = get_server_url(ping_addr.port())?; let open_path = fs::canonicalize(open_path)?; let open_path = path_to_url(&format!("{address}/fw/fsb"), None, &open_path); @@ -146,7 +154,7 @@ impl Cli { break 'err_print; } } - eprintln!("Failed to connect to server: {err}"); + eprintln!("Failed to connect to server at {addr}: {err}"); } } } @@ -182,7 +190,15 @@ impl Cli { } } process = match cmd - .args(["--port", &self.port.to_string(), "serve", "--log", "off"]) + .args([ + "--host", + &self.host.to_string(), + "--port", + &self.port.to_string(), + "serve", + "--log", + "off", + ]) // Subtle: the default of // `stdout(Stdio::inherit())` causes a parent // process to block, since the child process @@ -245,13 +261,15 @@ impl Cli { } Commands::Stop => { println!("Stopping server..."); + let stop_addr = fix_addr(addr); + // TODO: Use https://crates.io/crates/sysinfo to find the server // process and kill it if it doesn't respond to a stop request. - return match minreq::get(format!("http://{IP_ADDRESS}:{}/stop", self.port)) + return match minreq::get(format!("http://{stop_addr}/stop")) .with_timeout(3) .send() { - Err(err) => Err(format!("Failed to stop server: {err}").into()), + Err(err) => Err(format!("Failed to stop server at {stop_addr}: {err}").into()), Ok(response) => { let status_code = response.status_code; if status_code == 204 { @@ -273,10 +291,39 @@ impl Cli { } } +const PORT_RANGE: RangeInclusive = 1..=65535; + +// Copied from the [clap docs](https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#validated-values). +fn port_in_range(s: &str) -> Result { + let port: usize = s + .parse() + .map_err(|_| format!("`{s}` isn't a port number"))?; + if PORT_RANGE.contains(&port) { + Ok(port as u16) + } else { + Err(format!( + "port not in range {}-{}", + PORT_RANGE.start(), + PORT_RANGE.end() + )) + } +} + +fn fix_addr(addr: &SocketAddr) -> SocketAddr { + if addr.ip() == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) { + let mut addr = *addr; + addr.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + addr + } else { + *addr + } +} + #[cfg(not(tarpaulin_include))] fn main() -> Result<(), Box> { let cli = Cli::parse(); - cli.run()?; + let addr = SocketAddr::new(cli.host, cli.port); + cli.run(&addr)?; Ok(()) } diff --git a/server/src/webserver.rs b/server/src/webserver.rs index bb310ee8..ba30f1ca 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -29,6 +29,7 @@ mod vscode; use std::{ collections::{HashMap, HashSet}, env, fs, io, + net::SocketAddr, path::{self, MAIN_SEPARATOR_STR, Path, PathBuf}, str::FromStr, sync::{Arc, Mutex}, @@ -334,10 +335,6 @@ macro_rules! queue_send { /// Globals /// ------- -/// -/// The IP address on which the server listens for incoming connections. -pub const IP_ADDRESS: &str = "127.0.0.1"; - // The timeout for a reply from a websocket. Use a short timeout to speed up // unit tests. const REPLY_TIMEOUT: Duration = if cfg!(test) { @@ -1336,27 +1333,26 @@ async fn client_websocket( // Webserver core // -------------- #[actix_web::main] -pub async fn main(port: u16) -> std::io::Result<()> { - run_server(port).await +pub async fn main(addr: &SocketAddr) -> std::io::Result<()> { + run_server(addr).await } -pub async fn run_server(port: u16) -> std::io::Result<()> { +pub async fn run_server(addr: &SocketAddr) -> std::io::Result<()> { // Connect to the Capture Database //let _event_capture = EventCapture::new("config.json").await?; // Pre-load the bundled files before starting the webserver. let _ = &*BUNDLED_FILES_MAP; - let app_data = make_app_data(port); + let app_data = make_app_data(addr.port()); let app_data_server = app_data.clone(); - let server = match HttpServer::new(move || configure_app(App::new(), &app_data_server)) - .bind((IP_ADDRESS, port)) - { - Ok(server) => server.run(), - Err(err) => { - error!("Unable to bind to {IP_ADDRESS}:{port} - {err}"); - return Err(err); - } - }; + let server = + match HttpServer::new(move || configure_app(App::new(), &app_data_server)).bind(addr) { + Ok(server) => server.run(), + Err(err) => { + error!("Unable to bind to {addr} - {err}"); + return Err(err); + } + }; // Store the server handle in the global state. *(app_data.server_handle.lock().unwrap()) = Some(server.handle()); // Start the server. @@ -1659,6 +1655,7 @@ pub async fn get_server_url(port: u16) -> Result { )) } } else { - Ok(format!("http://{IP_ADDRESS}:{port}")) + // We're running locally, so use localhost. + Ok(format!("http://127.0.0.1:{port}")) } } diff --git a/server/src/webserver/vscode/tests.rs b/server/src/webserver/vscode/tests.rs index 8c14947f..f84b68af 100644 --- a/server/src/webserver/vscode/tests.rs +++ b/server/src/webserver/vscode/tests.rs @@ -20,6 +20,7 @@ use std::{ fs::{self, File}, io::{Error, Read}, + net::SocketAddr, path::{self, Path, PathBuf}, thread, time::{Duration, SystemTime}, @@ -46,9 +47,7 @@ use tokio_tungstenite::{ tungstenite::{http::StatusCode, protocol::Message}, }; -use super::super::{ - EditorMessage, EditorMessageContents, IP_ADDRESS, IdeType, run_server, tests::IP_PORT, -}; +use super::super::{EditorMessage, EditorMessageContents, IdeType, run_server, tests::IP_PORT}; use crate::{ cast, processing::{ @@ -64,7 +63,7 @@ use crate::{ lazy_static! { // Run a single webserver for all tests. static ref WEBSERVER_HANDLE: JoinHandle> = - actix_rt::spawn(async move { run_server(IP_PORT).await }); + actix_rt::spawn(async move { run_server(&SocketAddr::new("127.0.0.1".parse().unwrap(), IP_PORT)).await }); } // Send a message via a websocket. @@ -109,12 +108,10 @@ async fn read_message( type WebSocketStreamTcp = WebSocketStream>; async fn connect_async_server(prefix: &str, connection_id: &str) -> WebSocketStreamTcp { - connect_async(format!( - "ws://{IP_ADDRESS}:{IP_PORT}{prefix}/{connection_id}", - )) - .await - .expect("Failed to connect") - .0 + connect_async(format!("ws://127.0.0.1:{IP_PORT}{prefix}/{connection_id}",)) + .await + .expect("Failed to connect") + .0 } async fn connect_async_ide(connection_id: &str) -> WebSocketStreamTcp { @@ -185,7 +182,7 @@ async fn _prep_test( let _ = &*WEBSERVER_HANDLE; let now = SystemTime::now(); while now.elapsed().unwrap().as_millis() < 100 { - if minreq::get(format!("http://{IP_ADDRESS}:{IP_PORT}/ping",)) + if minreq::get(format!("http://127.0.0.1:{IP_PORT}/ping",)) .send() .is_ok() { @@ -223,7 +220,7 @@ async fn test_vscode_ide_websocket1() { // Start a second connection; verify that it fails. let err = connect_async(format!( - "ws://{IP_ADDRESS}:{IP_PORT}/vsc/ws-ide/{connection_id}", + "ws://127.0.0.1:{IP_PORT}/vsc/ws-ide/{connection_id}", )) .await .expect_err("Should fail to connect"); diff --git a/server/tests/cli.rs b/server/tests/cli.rs index c55efe3d..8243fc10 100644 --- a/server/tests/cli.rs +++ b/server/tests/cli.rs @@ -14,7 +14,6 @@ use assert_cmd::Command; use predicates::{prelude::predicate, str::contains}; // ### Local -use code_chat_editor::webserver::IP_ADDRESS; use tokio::task::spawn_blocking; // Support functions @@ -50,7 +49,7 @@ async fn test_start_no_response() { // Run a dummy webserver that doesn't respond to the `/stop` endpoint. actix_rt::spawn(async move { HttpServer::new(App::new) - .bind((IP_ADDRESS, 8082)) + .bind(("127.0.0.1", 8082)) .unwrap() .run() .await