+1








766fb96ff2
* Adding support for hosting with URL prefix to make reverse-proxy setup on sub-path possible
* Adjusting recent changes to auth.html (changing to relative URLs) after rebasing from upstream
* Adjusting service_worker and application manifest to support URL-prefix
* Add notes on Space Lua Pattern Matching (#1366)
* Remove common/space_lua/stdlib/math.md (#1371)
* Escape pipe symbol in string.gsub (#1370)
* feat: configure vim in CONFIG (#1369)
* Fix week calculation (#1368)
* Add more tests for week calculation
* Fix week calculation
* Add more tests
* Implement Lua built-in function os.difftime (#1367)
* Implement Lua built-in function os.difftime
* Test os.difftime
---------
Co-authored-by: Zef Hemel <zef@zef.me>
* Fix index page naming during server initialization (#1365)
* Allow non-zero shell exit codes (#1377)
* Generate version from git describe at build time (#1374)
* Generate version from git describe at build time
* Add 2.0-beta prefix
* Use Deno.Command instead of Deno.run
* Add missing case to math.random and check arguments (#1372)
* Fix paragraphs for the tag page (#1376)
* Render a predefined string for empty markdown table instead of pipe characters (#1387)
* Fix string.find (#1390)
* Fetch tags and history for proper version string generation (#1388)
* Fix support for disabling shortcuts (#1393)
* Add GitHub Actions Cache to reduce build times (#1394)
* Order strings in queries using collation algorithm (#1391)
Configurable with queryCollation, default value maintains old behaviour.
Added tests, schema, example settings, and documentation to query page.
Close: #614
Close: #1316
* Utility function to handle empty query results and templates (#1386)
* Add global Lua function 'some'
Moved documentation for 'each' function to non-standard section.
* Revert "Add optional argument to template.each for empty collection output (#1359)"
This reverts commit b73dadec67.
* Reimplement #1359 using 'some' function
* Update Lua to event.listEvents to match plugos syscall (#1397)
The documentation, example, and other function names use
`event.listEvents`, but `event.list` is being registered with the
client.
* Update widget header to not overflow container (#1410)
Also add a 10px margin below the header to maintain consistent spacing
around the widget contents.
* fix: case insensitive admonitions by default (#1409)
* fix: closing bracket is missing (#1408)
* Add optional priority attribute to PageCreatingContent (#1392)
* Add optional priority attribute to PageCreatingContent
* Minor simplification
* Fix comments
* Revert changes
* Add pageEventHandlers
* Use UTC year, month, and date for strings lacking time components (#1403)
* feat: allow CONFIG.md in any directory (#1398)
* fix: bundle external dependencies (#1379)
* Deno bump
* Copy-edit splash screen text (#1413)
* Fixes #1401 by implementing Lua's select
* Fixes offline capability of PWA (I hope).
Now storing config in localStorage.
* fix lint
* Reduce blast radius of Lua errors
* Fixes #1411: adds 'priority' setting for space style
* Refactor: Merging common/ into web/
* Fixes #1400
* Improves #1312 by rendering Lua expressions (but not widgets)
* Fix minor typos, add configuration to the docs
---------
Co-authored-by: wbhouston <wmbhouston@gmail.com>
Co-authored-by: Lennart Ochel <lennart.ochel@outlook.com>
Co-authored-by: Matthias Benaets <89214559+MatthiasBenaets@users.noreply.github.com>
Co-authored-by: Zef Hemel <zef@zef.me>
Co-authored-by: Marek S. Łukasiewicz <marek@lukasiewicz.tech>
Co-authored-by: Ivan Alejandro <ivanalejandro0@gmail.com>
Co-authored-by: Albert-Jan de Vries <aj+github@albert-jandevries.nl>
Co-authored-by: Cole Stowell <121599022+costowell@users.noreply.github.com>
Co-authored-by: Jim Tittsler <jimt@onjapan.net>
586 lines
17 KiB
TypeScript
586 lines
17 KiB
TypeScript
import { assert, assertEquals, assertExists } from "@std/assert";
|
|
import { HttpServer, type ServerOptions } from "./http_server.ts";
|
|
import { AssetBundle } from "$lib/asset_bundle/bundle.ts";
|
|
import { MemoryKvPrimitives } from "$lib/data/memory_kv_primitives.ts";
|
|
import { sleep } from "../lib/async.ts";
|
|
import {
|
|
ShellStreamClient,
|
|
type ShellStreamEvent,
|
|
} from "../web/shell_stream_client.ts";
|
|
import { HttpSpacePrimitives } from "../web/spaces/http_space_primitives.ts";
|
|
|
|
// Create a test script
|
|
async function createTestScript(dir: string): Promise<string> {
|
|
const scriptContent = `#!/bin/sh
|
|
echo "Hello from test script"
|
|
echo "This is stderr output" >&2
|
|
read input
|
|
echo "You said: $input"
|
|
exit 0
|
|
`;
|
|
|
|
const scriptPath = `${dir}/test_script.sh`;
|
|
await Deno.writeTextFile(scriptPath, scriptContent);
|
|
await Deno.chmod(scriptPath, 0o755); // Make executable
|
|
return scriptPath;
|
|
}
|
|
|
|
// Create a signal-handling test script
|
|
async function createSignalTestScript(dir: string): Promise<string> {
|
|
const scriptContent = `#!/bin/sh
|
|
echo "Starting signal test script"
|
|
# Set up signal handler
|
|
trap 'echo "Received SIGTERM"; exit 0' TERM
|
|
trap 'echo "Received SIGINT"; exit 0' INT
|
|
echo "Signal handlers set up"
|
|
# Wait indefinitely
|
|
while true; do
|
|
sleep 1
|
|
done
|
|
`;
|
|
|
|
const scriptPath = `${dir}/signal_test.sh`;
|
|
await Deno.writeTextFile(scriptPath, scriptContent);
|
|
await Deno.chmod(scriptPath, 0o755); // Make executable
|
|
return scriptPath;
|
|
}
|
|
|
|
// Create a long-running test script that writes to a file when it exits
|
|
async function createLongRunningScript(dir: string): Promise<string> {
|
|
const scriptContent = `#!/bin/sh
|
|
echo "Starting long-running script"
|
|
# Create a file to track the process ID
|
|
echo $$ > "${dir}/process.pid"
|
|
# Set up signal handler to detect proper cleanup
|
|
trap 'echo "Process terminated properly" > "${dir}/cleanup.log"; exit 0' TERM INT
|
|
echo "Ready for long-running operation"
|
|
# Wait indefinitely
|
|
while true; do
|
|
sleep 1
|
|
echo "Still running..."
|
|
done
|
|
`;
|
|
|
|
const scriptPath = `${dir}/long_running.sh`;
|
|
await Deno.writeTextFile(scriptPath, scriptContent);
|
|
await Deno.chmod(scriptPath, 0o755); // Make executable
|
|
return scriptPath;
|
|
}
|
|
|
|
// Setup a minimal HttpServer for testing
|
|
async function setupTestServer(
|
|
tempDir: string,
|
|
): Promise<{ server: HttpServer; port: number }> {
|
|
// Create minimal asset bundles
|
|
const clientAssetBundle = new AssetBundle();
|
|
const plugAssetBundle = new AssetBundle();
|
|
|
|
// Create a memory KV store
|
|
const kvPrimitives = new MemoryKvPrimitives();
|
|
|
|
// Find an available port
|
|
const port = 9000 + Math.floor(Math.random() * 1000);
|
|
|
|
// Create server options
|
|
const options: ServerOptions = {
|
|
hostname: "localhost",
|
|
port,
|
|
clientAssetBundle,
|
|
plugAssetBundle,
|
|
baseKvPrimitives: kvPrimitives,
|
|
pagesPath: tempDir,
|
|
shellBackend: "local", // Use local shell backend
|
|
readOnly: false,
|
|
indexPage: "index",
|
|
enableSpaceScript: false,
|
|
hostUrlPrefix: undefined,
|
|
};
|
|
|
|
// Create and start the server
|
|
const server = new HttpServer(options);
|
|
await server.start();
|
|
|
|
return { server, port };
|
|
}
|
|
|
|
// Helper function to create a script that fails with an error
|
|
async function createErrorScript(dir: string): Promise<string> {
|
|
const scriptContent = `#!/bin/sh
|
|
echo "This is stdout before the error"
|
|
echo "This is stderr output" >&2
|
|
exit 1
|
|
`;
|
|
|
|
const scriptPath = `${dir}/error_script.sh`;
|
|
await Deno.writeTextFile(scriptPath, scriptContent);
|
|
await Deno.chmod(scriptPath, 0o755); // Make executable
|
|
return scriptPath;
|
|
}
|
|
|
|
// Run all tests in a single test function to avoid resource tracking issues
|
|
// TODO: Fix this, make this more stable
|
|
Deno.test("Shell endpoint tests", { ignore: true }, async () => {
|
|
// Test 1: Basic functionality with POST endpoint
|
|
{
|
|
// Test: Basic functionality with POST endpoint
|
|
// Create a temporary directory for testing
|
|
const tempDir = await Deno.makeTempDir();
|
|
|
|
try {
|
|
// Set up test server
|
|
const { server, port } = await setupTestServer(tempDir);
|
|
|
|
try {
|
|
// Create a client with HttpSpacePrimitives
|
|
const httpSpacePrimitives = new HttpSpacePrimitives(
|
|
`http://localhost:${port}`,
|
|
);
|
|
const client = new ShellStreamClient({
|
|
httpSpacePrimitives,
|
|
cmd: "echo",
|
|
args: ["Hello, World!"],
|
|
});
|
|
|
|
// Collect events
|
|
const events: ShellStreamEvent[] = [];
|
|
client.addEventListener("stdout", (event) => events.push(event));
|
|
client.addEventListener("stderr", (event) => events.push(event));
|
|
client.addEventListener("exit", (event) => events.push(event));
|
|
|
|
// Connect to the server
|
|
await client.start();
|
|
|
|
// Wait for output and exit
|
|
await sleep(1000);
|
|
|
|
// Close the connection and wait for cleanup
|
|
client.close();
|
|
await sleep(500);
|
|
|
|
// Verify events
|
|
assert(events.length >= 1, "Expected at least 1 event");
|
|
|
|
// Should have stdout message
|
|
const stdoutEvent = events.find((e) =>
|
|
e.type === "stdout" &&
|
|
typeof e.data === "string" &&
|
|
e.data.includes("Hello, World!")
|
|
);
|
|
assertExists(stdoutEvent, "Stdout event not found");
|
|
} finally {
|
|
// Clean up server
|
|
server.stop();
|
|
}
|
|
} finally {
|
|
// Clean up temporary directory
|
|
await Deno.remove(tempDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// Cleanup on premature WebSocket close
|
|
{
|
|
// Test: Cleanup on premature WebSocket close
|
|
// Create a temporary directory for testing
|
|
const tempDir = await Deno.makeTempDir();
|
|
|
|
try {
|
|
// Create a long-running test script
|
|
const scriptPath = await createLongRunningScript(tempDir);
|
|
|
|
// Set up test server
|
|
const { server, port } = await setupTestServer(tempDir);
|
|
|
|
try {
|
|
// Create a client with HttpSpacePrimitives
|
|
const httpSpacePrimitives = new HttpSpacePrimitives(
|
|
`http://localhost:${port}`,
|
|
);
|
|
const client = new ShellStreamClient({
|
|
httpSpacePrimitives,
|
|
cmd: scriptPath,
|
|
args: [],
|
|
});
|
|
|
|
// Collect events
|
|
const events: ShellStreamEvent[] = [];
|
|
client.addEventListener("stdout", (event) => events.push(event));
|
|
client.addEventListener("stderr", (event) => events.push(event));
|
|
client.addEventListener("exit", (event) => events.push(event));
|
|
|
|
// Connect to the server
|
|
await client.start();
|
|
|
|
// Wait for the script to start
|
|
await sleep(1000);
|
|
|
|
// Verify the process is running by checking if the PID file exists
|
|
const pidFileExists = await Deno.stat(`${tempDir}/process.pid`)
|
|
.then(
|
|
() => true,
|
|
() => false,
|
|
);
|
|
assert(pidFileExists, "Process PID file should exist");
|
|
|
|
// Close the WebSocket connection prematurely
|
|
client.close();
|
|
|
|
// Wait for cleanup to happen
|
|
await sleep(2000);
|
|
|
|
// Check if the cleanup log file was created, indicating proper termination
|
|
const cleanupFileExists = await Deno.stat(`${tempDir}/cleanup.log`)
|
|
.then(
|
|
() => true,
|
|
() => false,
|
|
);
|
|
assert(
|
|
cleanupFileExists,
|
|
"Cleanup log file should exist, indicating proper process termination",
|
|
);
|
|
|
|
if (cleanupFileExists) {
|
|
const cleanupContent = await Deno.readTextFile(
|
|
`${tempDir}/cleanup.log`,
|
|
);
|
|
assert(
|
|
cleanupContent.includes("Process terminated properly"),
|
|
"Process should have been terminated properly",
|
|
);
|
|
}
|
|
|
|
// Verify events
|
|
const startupEvent = events.find((e) =>
|
|
e.type === "stdout" &&
|
|
typeof e.data === "string" &&
|
|
e.data.includes("Starting long-running script")
|
|
);
|
|
assertExists(startupEvent, "Startup message not found");
|
|
} finally {
|
|
// Clean up server
|
|
server.stop();
|
|
}
|
|
} finally {
|
|
// Clean up temporary directory
|
|
await Deno.remove(tempDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// Unified send method with POST endpoint
|
|
{
|
|
// Test: Unified send method with POST endpoint
|
|
// Create a temporary directory for testing
|
|
const tempDir = await Deno.makeTempDir();
|
|
|
|
try {
|
|
// Create a test script
|
|
const scriptPath = await createTestScript(tempDir);
|
|
|
|
// Set up test server
|
|
const { server, port } = await setupTestServer(tempDir);
|
|
|
|
try {
|
|
// Create a client with HttpSpacePrimitives
|
|
const httpSpacePrimitives = new HttpSpacePrimitives(
|
|
`http://localhost:${port}`,
|
|
);
|
|
const client = new ShellStreamClient({
|
|
httpSpacePrimitives,
|
|
cmd: scriptPath,
|
|
args: [],
|
|
});
|
|
|
|
// Collect events
|
|
const events: ShellStreamEvent[] = [];
|
|
client.addEventListener("stdout", (event) => events.push(event));
|
|
client.addEventListener("stderr", (event) => events.push(event));
|
|
client.addEventListener("exit", (event) => events.push(event));
|
|
|
|
// Connect to the server
|
|
await client.start();
|
|
|
|
// Wait for initial output
|
|
await sleep(500);
|
|
|
|
// Send input using the unified send() method
|
|
client.send("Hello from test");
|
|
|
|
// Wait for response and exit
|
|
await sleep(1000);
|
|
|
|
// Close the connection and wait for cleanup
|
|
client.close();
|
|
await sleep(500);
|
|
|
|
// Verify events
|
|
assert(events.length >= 3, "Expected at least 3 events");
|
|
|
|
// Should have stdout message
|
|
const stdoutEvent = events.find((e) =>
|
|
e.type === "stdout" &&
|
|
typeof e.data === "string" &&
|
|
e.data.includes("Hello from test script")
|
|
);
|
|
assertExists(stdoutEvent, "Stdout event not found");
|
|
|
|
// Should have stderr message
|
|
const stderrEvent = events.find((e) =>
|
|
e.type === "stderr" &&
|
|
typeof e.data === "string" &&
|
|
e.data.includes("This is stderr output")
|
|
);
|
|
assertExists(stderrEvent, "Stderr event not found");
|
|
|
|
// Should have response to input
|
|
const responseEvent = events.find((e) =>
|
|
e.type === "stdout" &&
|
|
typeof e.data === "string" &&
|
|
e.data.includes("You said: Hello from test")
|
|
);
|
|
assertExists(responseEvent, "Response event not found");
|
|
} finally {
|
|
// Clean up server
|
|
server.stop();
|
|
}
|
|
} finally {
|
|
// Clean up temporary directory
|
|
await Deno.remove(tempDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// Send signal to process
|
|
{
|
|
// Test: Send signal to process
|
|
// Create a temporary directory for testing
|
|
const tempDir = await Deno.makeTempDir();
|
|
|
|
try {
|
|
// Create a signal-handling test script
|
|
const scriptPath = await createSignalTestScript(tempDir);
|
|
|
|
// Set up test server
|
|
const { server, port } = await setupTestServer(tempDir);
|
|
|
|
try {
|
|
// Create a client with HttpSpacePrimitives
|
|
const httpSpacePrimitives = new HttpSpacePrimitives(
|
|
`http://localhost:${port}`,
|
|
);
|
|
const client = new ShellStreamClient({
|
|
httpSpacePrimitives,
|
|
cmd: scriptPath,
|
|
args: [],
|
|
});
|
|
|
|
// Collect events
|
|
const events: ShellStreamEvent[] = [];
|
|
client.addEventListener("stdout", (event) => events.push(event));
|
|
client.addEventListener("stderr", (event) => events.push(event));
|
|
client.addEventListener("exit", (event) => events.push(event));
|
|
|
|
// Connect to the server
|
|
await client.start();
|
|
|
|
// Wait for the script to start and set up signal handlers
|
|
await sleep(1000);
|
|
|
|
// Send a signal to the process
|
|
client.kill("SIGTERM");
|
|
|
|
// Wait for the process to handle the signal and exit
|
|
await sleep(1000);
|
|
|
|
// Close the connection and wait for cleanup
|
|
client.close();
|
|
await sleep(500);
|
|
|
|
// Verify events
|
|
assert(events.length >= 3, "Expected at least 3 events");
|
|
|
|
// Should have initial stdout messages
|
|
const startupEvent = events.find((e) =>
|
|
e.type === "stdout" &&
|
|
typeof e.data === "string" &&
|
|
e.data.includes("Starting signal test script")
|
|
);
|
|
assertExists(startupEvent, "Startup message not found");
|
|
|
|
// Should have signal handler message
|
|
const signalEvent = events.find((e) =>
|
|
e.type === "stdout" &&
|
|
typeof e.data === "string" &&
|
|
e.data.includes("Received SIGTERM")
|
|
);
|
|
assertExists(signalEvent, "Signal handler message not found");
|
|
|
|
// Should have exit event
|
|
const exitEvent = events.find((e) => e.type === "exit");
|
|
assertExists(exitEvent, "Exit event not found");
|
|
} finally {
|
|
// Clean up server
|
|
server.stop();
|
|
}
|
|
} finally {
|
|
// Clean up temporary directory
|
|
await Deno.remove(tempDir, { recursive: true });
|
|
}
|
|
}
|
|
// .shell command with arguments
|
|
{
|
|
// Test: Command with arguments
|
|
// Create a temporary directory for testing
|
|
const tempDir = await Deno.makeTempDir();
|
|
|
|
try {
|
|
// Create a test script
|
|
const scriptPath = await createTestScript(tempDir);
|
|
|
|
// Set up test server
|
|
const { server, port } = await setupTestServer(tempDir);
|
|
|
|
try {
|
|
// Create HTTP space primitives for making requests
|
|
const httpSpacePrimitives = new HttpSpacePrimitives(
|
|
`http://localhost:${port}`,
|
|
);
|
|
|
|
// Test command with arguments
|
|
const response = await httpSpacePrimitives.authenticatedFetch(
|
|
`${httpSpacePrimitives.url}/.shell`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
cmd: scriptPath,
|
|
args: [],
|
|
}),
|
|
},
|
|
);
|
|
|
|
const result = await response.json();
|
|
|
|
// Verify response
|
|
assertEquals(result.code, 0, "Exit code should be 0");
|
|
assert(
|
|
result.stdout.includes("Hello from test script"),
|
|
"Output should contain the script output",
|
|
);
|
|
assert(
|
|
result.stderr.includes("This is stderr output"),
|
|
"Stderr should contain the error output",
|
|
);
|
|
} finally {
|
|
// Clean up server
|
|
server.stop();
|
|
}
|
|
} finally {
|
|
// Clean up temporary directory
|
|
await Deno.remove(tempDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// Error handling
|
|
{
|
|
// Test: Error handling
|
|
// Create a temporary directory for testing
|
|
const tempDir = await Deno.makeTempDir();
|
|
|
|
try {
|
|
// Create an error script
|
|
const scriptPath = await createErrorScript(tempDir);
|
|
|
|
// Set up test server
|
|
const { server, port } = await setupTestServer(tempDir);
|
|
|
|
try {
|
|
// Create HTTP space primitives for making requests
|
|
const httpSpacePrimitives = new HttpSpacePrimitives(
|
|
`http://localhost:${port}`,
|
|
);
|
|
|
|
// Test command that fails
|
|
const response = await httpSpacePrimitives.authenticatedFetch(
|
|
`${httpSpacePrimitives.url}/.shell`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
cmd: scriptPath,
|
|
args: [],
|
|
}),
|
|
},
|
|
);
|
|
|
|
const result = await response.json();
|
|
|
|
// Verify response
|
|
assertEquals(result.code, 1, "Exit code should be 1");
|
|
assert(
|
|
result.stdout.includes("This is stdout before the error"),
|
|
"Output should contain the stdout text",
|
|
);
|
|
assert(
|
|
result.stderr.includes("This is stderr output"),
|
|
"Stderr should contain the error output",
|
|
);
|
|
} finally {
|
|
// Clean up server
|
|
server.stop();
|
|
}
|
|
} finally {
|
|
// Clean up temporary directory
|
|
await Deno.remove(tempDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// Read-only mode
|
|
{
|
|
// Test: Read-only mode
|
|
// Create a temporary directory for testing
|
|
const tempDir = await Deno.makeTempDir();
|
|
|
|
try {
|
|
// Set up test server with read-only mode
|
|
const { server, port } = await setupTestServer(tempDir);
|
|
|
|
// Set server to read-only mode
|
|
server.spaceServer.readOnly = true;
|
|
|
|
try {
|
|
// Create HTTP space primitives for making requests
|
|
const httpSpacePrimitives = new HttpSpacePrimitives(
|
|
`http://localhost:${port}`,
|
|
);
|
|
|
|
// Test command in read-only mode
|
|
const response = await httpSpacePrimitives.authenticatedFetch(
|
|
`${httpSpacePrimitives.url}/.shell`,
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
cmd: "echo",
|
|
args: ["Hello, World!"],
|
|
}),
|
|
},
|
|
);
|
|
|
|
// Verify response status is 405 Method Not Allowed
|
|
assertEquals(
|
|
response.status,
|
|
405,
|
|
"Status should be 405 Method Not Allowed",
|
|
);
|
|
|
|
const text = await response.text();
|
|
assert(
|
|
text.includes("Read only mode"),
|
|
"Response should mention read-only mode",
|
|
);
|
|
} finally {
|
|
// Clean up server
|
|
server.stop();
|
|
}
|
|
} finally {
|
|
// Clean up temporary directory
|
|
await Deno.remove(tempDir, { recursive: true });
|
|
}
|
|
}
|
|
});
|