feat: add networking module with TCP Socket/Server and example applications
Implement a full-featured `net` module supporting TCP `Socket` and `Server` classes via libuv, including example applications (chat server, echo server, HTTP client/server, file explorer, system dashboard) and benchmark scripts (fibonacci, n-body). Add GEMINI.md project documentation and Stderr class to io module.
This commit is contained in:
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
import "os" for Process
|
||||
|
||||
var start = System.clock
|
||||
|
||||
var fib
|
||||
fib = Fn.new { |n|
|
||||
if (n < 2) return n
|
||||
return fib.call(n - 1) + fib.call(n - 2)
|
||||
}
|
||||
|
||||
for (i in 1..5) {
|
||||
System.print("fib(28) = " + fib.call(28).toString)
|
||||
}
|
||||
|
||||
System.print("Elapsed: " + (System.clock - start).toString + "s")
|
||||
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
import "os" for Process
|
||||
|
||||
class Body {
|
||||
construct new(x, y, z, vx, vy, vz, mass) {
|
||||
_x = x
|
||||
_y = y
|
||||
_z = z
|
||||
_vx = vx
|
||||
_vy = vy
|
||||
_vz = vz
|
||||
_mass = mass
|
||||
}
|
||||
|
||||
x { _x }
|
||||
x=(v) { _x = v }
|
||||
y { _y }
|
||||
y=(v) { _y = v }
|
||||
z { _z }
|
||||
z=(v) { _z = v }
|
||||
vx { _vx }
|
||||
vx=(v) { _vx = v }
|
||||
vy { _vy }
|
||||
vy=(v) { _vy = v }
|
||||
vz { _vz }
|
||||
vz=(v) { _vz = v }
|
||||
mass { _mass }
|
||||
|
||||
offsetMomentum(px, py, pz) {
|
||||
_vx = -px / 0.01227246237529089
|
||||
_vy = -py / 0.01227246237529089
|
||||
_vz = -pz / 0.01227246237529089
|
||||
}
|
||||
}
|
||||
|
||||
class NBody {
|
||||
static init() {
|
||||
var pi = 3.141592653589793
|
||||
var solarMass = 4 * pi * pi
|
||||
var daysPerYear = 365.24
|
||||
|
||||
__bodies = [
|
||||
// Sun
|
||||
Body.new(0, 0, 0, 0, 0, 0, solarMass),
|
||||
// Jupiter
|
||||
Body.new(
|
||||
4.84143144246472090e+00,
|
||||
-1.16032004402742839e+00,
|
||||
-1.03622044471123109e-01,
|
||||
1.66007664274403694e-03 * daysPerYear,
|
||||
7.69901118419740425e-03 * daysPerYear,
|
||||
-6.90460016972063023e-05 * daysPerYear,
|
||||
9.54791938424326609e-04 * solarMass),
|
||||
// Saturn
|
||||
Body.new(
|
||||
8.34336671824457987e+00,
|
||||
4.12479856412430479e+00,
|
||||
-4.03523417114321381e-01,
|
||||
-2.76742510726862411e-03 * daysPerYear,
|
||||
4.99852801234917238e-03 * daysPerYear,
|
||||
2.30417297573763929e-05 * daysPerYear,
|
||||
2.85885980666130812e-04 * solarMass),
|
||||
// Uranus
|
||||
Body.new(
|
||||
1.28943695618239209e+01,
|
||||
-1.51111514016986312e+01,
|
||||
-2.23307578892655734e-01,
|
||||
2.96460137564761618e-03 * daysPerYear,
|
||||
2.37847173959480950e-03 * daysPerYear,
|
||||
-2.96589568540237556e-05 * daysPerYear,
|
||||
4.36624404335156298e-05 * solarMass),
|
||||
// Neptune
|
||||
Body.new(
|
||||
1.53796971148509165e+01,
|
||||
-2.59193146099879641e+01,
|
||||
1.79258772950371181e-01,
|
||||
2.68067772490389322e-03 * daysPerYear,
|
||||
1.62824170038242295e-03 * daysPerYear,
|
||||
-9.51592254519715870e-05 * daysPerYear,
|
||||
5.15138902046611451e-05 * solarMass)
|
||||
]
|
||||
|
||||
var px = 0
|
||||
var py = 0
|
||||
var pz = 0
|
||||
for (b in __bodies) {
|
||||
px = px + b.vx * b.mass
|
||||
py = py + b.vy * b.mass
|
||||
pz = pz + b.vz * b.mass
|
||||
}
|
||||
__bodies[0].offsetMomentum(px, py, pz)
|
||||
}
|
||||
|
||||
static energy() {
|
||||
var e = 0
|
||||
for (i in 0...__bodies.count) {
|
||||
var bi = __bodies[i]
|
||||
e = e + 0.5 * bi.mass * (bi.vx * bi.vx + bi.vy * bi.vy + bi.vz * bi.vz)
|
||||
for (j in i + 1...__bodies.count) {
|
||||
var bj = __bodies[j]
|
||||
var dx = bi.x - bj.x
|
||||
var dy = bi.y - bj.y
|
||||
var dz = bi.z - bj.z
|
||||
var distance = (dx * dx + dy * dy + dz * dz).sqrt
|
||||
e = e - (bi.mass * bj.mass) / distance
|
||||
}
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
static advance(dt) {
|
||||
for (i in 0...__bodies.count) {
|
||||
var bi = __bodies[i]
|
||||
for (j in i + 1...__bodies.count) {
|
||||
var bj = __bodies[j]
|
||||
var dx = bi.x - bj.x
|
||||
var dy = bi.y - bj.y
|
||||
var dz = bi.z - bj.z
|
||||
var d2 = dx * dx + dy * dy + dz * dz
|
||||
var mag = dt / (d2 * d2.sqrt)
|
||||
|
||||
bi.vx = bi.vx - dx * bj.mass * mag
|
||||
bi.vy = bi.vy - dy * bj.mass * mag
|
||||
bi.vz = bi.vz - dz * bj.mass * mag
|
||||
|
||||
bj.vx = bj.vx + dx * bi.mass * mag
|
||||
bj.vy = bj.vy + dy * bi.mass * mag
|
||||
bj.vz = bj.vz + dz * bi.mass * mag
|
||||
}
|
||||
}
|
||||
|
||||
for (b in __bodies) {
|
||||
b.x = b.x + dt * b.vx
|
||||
b.y = b.y + dt * b.vy
|
||||
b.z = b.z + dt * b.vz
|
||||
}
|
||||
}
|
||||
|
||||
static run(n) {
|
||||
init()
|
||||
System.print(energy())
|
||||
for (i in 0...n) advance(0.01)
|
||||
System.print(energy())
|
||||
}
|
||||
}
|
||||
|
||||
var start = System.clock
|
||||
NBody.run(100000)
|
||||
System.print("Elapsed: " + (System.clock - start).toString + "s")
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
import "net" for Server, Socket
|
||||
import "scheduler" for Scheduler
|
||||
|
||||
class ChatServer {
|
||||
construct new(port) {
|
||||
_port = port
|
||||
_clients = []
|
||||
_server = Server.bind("0.0.0.0", port)
|
||||
System.print("Chat Server listening on port %(_port)")
|
||||
}
|
||||
|
||||
run() {
|
||||
while (true) {
|
||||
var socket = _server.accept()
|
||||
handleNewClient(socket)
|
||||
}
|
||||
}
|
||||
|
||||
handleNewClient(socket) {
|
||||
_clients.add(socket)
|
||||
var id = _clients.count
|
||||
System.print("Client %(id) connected. Total: %(_clients.count)")
|
||||
|
||||
Fiber.new {
|
||||
broadcast("Client %(id) joined the chat!\n", socket)
|
||||
|
||||
while (true) {
|
||||
var message = socket.read()
|
||||
if (message == null || message == "") break
|
||||
|
||||
System.print("Client %(id): %(message.trim())")
|
||||
broadcast("Client %(id): %(message)", socket)
|
||||
}
|
||||
|
||||
System.print("Client %(id) disconnected.")
|
||||
_clients.remove(socket)
|
||||
socket.close()
|
||||
broadcast("Client %(id) left the chat.\n", null)
|
||||
}.call()
|
||||
}
|
||||
|
||||
broadcast(message, sender) {
|
||||
for (client in _clients) {
|
||||
if (client != sender) {
|
||||
// We use a separate fiber for each write to prevent one slow
|
||||
// client from blocking the broadcast to others
|
||||
Fiber.new {
|
||||
client.write(message)
|
||||
}.call()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var server = ChatServer.new(7070)
|
||||
server.run()
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
import "net" for Server
|
||||
import "scheduler" for Scheduler
|
||||
|
||||
var port = 9090
|
||||
var server = Server.bind("0.0.0.0", port)
|
||||
System.print("Echo Server running on port %(port)")
|
||||
|
||||
while (true) {
|
||||
var socket = server.accept()
|
||||
System.print("New connection accepted")
|
||||
|
||||
Fiber.new {
|
||||
var socket_ = socket // Capture variable
|
||||
while (true) {
|
||||
var data = socket_.read()
|
||||
if (data == null) {
|
||||
break
|
||||
}
|
||||
System.print("Received: %(data.count) bytes")
|
||||
socket_.write(data)
|
||||
}
|
||||
System.print("Connection closed")
|
||||
socket_.close()
|
||||
}.call()
|
||||
}
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
import "io" for Directory, File, Stat
|
||||
|
||||
class FileExplorer {
|
||||
static listRecursive(path, depth) {
|
||||
var indent = " " * depth
|
||||
var files = []
|
||||
|
||||
// We wrap Directory.list in a try to handle permission errors
|
||||
var fiber = Fiber.new {
|
||||
files = Directory.list(path)
|
||||
}
|
||||
fiber.try()
|
||||
|
||||
if (fiber.error != null) {
|
||||
System.print("\%(indent)[!] Error accessing \%(path)")
|
||||
return
|
||||
}
|
||||
|
||||
// Sort files manually since String comparison isn't built-in
|
||||
files.sort(Fn.new {|a, b|
|
||||
var ba = a.bytes
|
||||
var bb = b.bytes
|
||||
var len = ba.count < bb.count ? ba.count : bb.count
|
||||
for (i in 0...len) {
|
||||
if (ba[i] < bb[i]) return true
|
||||
if (ba[i] > bb[i]) return false
|
||||
}
|
||||
return ba.count < bb.count
|
||||
})
|
||||
|
||||
for (file in files) {
|
||||
if (file == "." || file == "..") continue
|
||||
|
||||
var fullPath = path + "/" + file
|
||||
if (path == ".") fullPath = "./" + file
|
||||
|
||||
var stat = Stat.path(fullPath)
|
||||
if (stat.isDirectory) {
|
||||
System.print("\%(indent)[D] \%(file)/")
|
||||
listRecursive(fullPath, depth + 1)
|
||||
} else {
|
||||
var size = stat.size
|
||||
var unit = "B"
|
||||
if (size > 1024) {
|
||||
size = size / 1024
|
||||
unit = "KB"
|
||||
}
|
||||
System.print("\%(indent)[F] \%(file) (\%(size.round)\%(unit))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.print("Recursive directory listing for current path:")
|
||||
System.print("--------------------------------------------")
|
||||
FileExplorer.listRecursive(".", 0)
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
import "net" for Socket
|
||||
import "scheduler" for Scheduler
|
||||
|
||||
class HttpClient {
|
||||
static get(host, port, path) {
|
||||
System.print("Connecting to %(host):%(port)...")
|
||||
var socket = Socket.connect(host, port)
|
||||
|
||||
var request = "GET %(path) HTTP/1.1\r\n" +
|
||||
"Host: %(host)\r\n" +
|
||||
"User-Agent: Wren-CLI\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
"\r\n"
|
||||
|
||||
System.print("Sending request...")
|
||||
socket.write(request)
|
||||
|
||||
System.print("Waiting for response...")
|
||||
var response = ""
|
||||
while (true) {
|
||||
var chunk = socket.read()
|
||||
if (chunk == null) break
|
||||
response = response + chunk
|
||||
}
|
||||
|
||||
socket.close()
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage: Fetching from a local server or a known public IP
|
||||
// Note: This implementation is for raw IP/DNS if supported by uv_tcp_connect.
|
||||
// Since our net.c uses uv_ip4_addr, we use localhost for the example.
|
||||
var host = "127.0.0.1"
|
||||
var port = 8080 // Try to connect to our own http_server example
|
||||
var path = "/"
|
||||
|
||||
var result = HttpClient.get(host, port, path)
|
||||
System.print("\n--- Response Received ---
|
||||
")
|
||||
System.print(result)
|
||||
Vendored
+190
@@ -0,0 +1,190 @@
|
||||
import "net" for Server, Socket
|
||||
import "io" for File, Directory, Stat
|
||||
import "scheduler" for Scheduler
|
||||
|
||||
class HttpServer {
|
||||
construct new(port) {
|
||||
_port = port
|
||||
_server = Server.bind("0.0.0.0", port)
|
||||
System.print("HTTP Server listening on http://localhost:%(port)")
|
||||
}
|
||||
|
||||
run() {
|
||||
while (true) {
|
||||
var socket = _server.accept()
|
||||
Fiber.new {
|
||||
handleClient_(socket)
|
||||
}.call()
|
||||
}
|
||||
}
|
||||
|
||||
handleClient_(socket) {
|
||||
var requestData = socket.read()
|
||||
if (requestData == null || requestData == "") {
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
|
||||
var lines = requestData.split("\r\n")
|
||||
if (lines.count == 0) {
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
|
||||
var requestLine = lines[0].split(" ")
|
||||
if (requestLine.count < 2) {
|
||||
sendError_(socket, 400, "Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
var method = requestLine[0]
|
||||
var path = requestLine[1]
|
||||
|
||||
System.print("%(method) %(path)")
|
||||
|
||||
if (method != "GET") {
|
||||
sendError_(socket, 405, "Method Not Allowed")
|
||||
return
|
||||
}
|
||||
|
||||
path = path.replace("\%20", " ")
|
||||
|
||||
if (path.contains("..")) {
|
||||
sendError_(socket, 403, "Forbidden")
|
||||
return
|
||||
}
|
||||
|
||||
var localPath = "." + path
|
||||
if (localPath.endsWith("/")) localPath = localPath[0..-2]
|
||||
if (localPath == "") localPath = "."
|
||||
|
||||
if (!exists(localPath)) {
|
||||
sendError_(socket, 404, "Not Found")
|
||||
return
|
||||
}
|
||||
|
||||
if (isDirectory(localPath)) {
|
||||
serveDirectory_(socket, localPath, path)
|
||||
} else {
|
||||
serveFile_(socket, localPath)
|
||||
}
|
||||
|
||||
socket.close()
|
||||
}
|
||||
|
||||
exists(path) {
|
||||
if (File.exists(path)) return true
|
||||
if (Directory.exists(path)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
isDirectory(path) {
|
||||
return Directory.exists(path)
|
||||
}
|
||||
|
||||
serveDirectory_(socket, localPath, requestPath) {
|
||||
var files
|
||||
var fiber = Fiber.new {
|
||||
files = Directory.list(localPath)
|
||||
}
|
||||
fiber.try()
|
||||
|
||||
if (fiber.error != null) {
|
||||
sendError_(socket, 500, "Internal Server Error: " + fiber.error)
|
||||
return
|
||||
}
|
||||
|
||||
files.sort(Fn.new {|a, b|
|
||||
var ba = a.bytes
|
||||
var bb = b.bytes
|
||||
var len = ba.count
|
||||
if (bb.count < len) len = bb.count
|
||||
|
||||
for (i in 0...len) {
|
||||
if (ba[i] < bb[i]) return true
|
||||
if (ba[i] > bb[i]) return false
|
||||
}
|
||||
|
||||
return ba.count < bb.count
|
||||
})
|
||||
|
||||
var html = "<!DOCTYPE html><html><head><title>Index of %(requestPath)</title></head><body>"
|
||||
html = html + "<h1>Index of %(requestPath)</h1><hr><ul>"
|
||||
|
||||
if (requestPath != "/") {
|
||||
var parent = requestPath.split("/")
|
||||
if (parent.count > 1) {
|
||||
parent.removeAt(-1)
|
||||
var parentPath = parent.join("/")
|
||||
if (parentPath == "") parentPath = "/"
|
||||
html = html + "<li><a href=\"%(parentPath)\">..</a></li>"
|
||||
}
|
||||
}
|
||||
|
||||
for (file in files) {
|
||||
var href = requestPath
|
||||
if (!href.endsWith("/")) href = href + "/"
|
||||
href = href + file
|
||||
html = html + "<li><a href=\"%(href)\">%(file)</a></li>"
|
||||
}
|
||||
|
||||
html = html + "</ul><hr></body></html>"
|
||||
|
||||
sendResponse_(socket, 200, "OK", "text/html", html)
|
||||
}
|
||||
|
||||
serveFile_(socket, localPath) {
|
||||
var content
|
||||
var fiber = Fiber.new {
|
||||
content = File.read(localPath)
|
||||
}
|
||||
fiber.try()
|
||||
|
||||
if (fiber.error != null) {
|
||||
sendError_(socket, 500, "Error reading file: " + fiber.error)
|
||||
return
|
||||
}
|
||||
|
||||
var contentType = "application/octet-stream"
|
||||
if (localPath.endsWith(".html")) {
|
||||
contentType = "text/html"
|
||||
} else if (localPath.endsWith(".txt")) {
|
||||
contentType = "text/plain"
|
||||
} else if (localPath.endsWith(".wren")) {
|
||||
contentType = "text/plain"
|
||||
} else if (localPath.endsWith(".c")) {
|
||||
contentType = "text/plain"
|
||||
} else if (localPath.endsWith(".h")) {
|
||||
contentType = "text/plain"
|
||||
} else if (localPath.endsWith(".md")) {
|
||||
contentType = "text/markdown"
|
||||
} else if (localPath.endsWith(".json")) {
|
||||
contentType = "application/json"
|
||||
} else if (localPath.endsWith(".png")) {
|
||||
contentType = "image/png"
|
||||
} else if (localPath.endsWith(".jpg")) {
|
||||
contentType = "image/jpeg"
|
||||
}
|
||||
|
||||
sendResponse_(socket, 200, "OK", contentType, content)
|
||||
}
|
||||
|
||||
sendError_(socket, code, message) {
|
||||
var html = "<h1>%(code) %(message)</h1>"
|
||||
sendResponse_(socket, code, message, "text/html", html)
|
||||
socket.close()
|
||||
}
|
||||
|
||||
sendResponse_(socket, code, status, contentType, body) {
|
||||
var response = "HTTP/1.1 %(code) %(status)\r\n" +
|
||||
"Content-Type: %(contentType)\r\n" +
|
||||
"Content-Length: %(body.count)\r\n" +
|
||||
"Connection: close\r\n" +
|
||||
"\r\n" +
|
||||
body
|
||||
socket.write(response)
|
||||
}
|
||||
}
|
||||
|
||||
var server = HttpServer.new(8080)
|
||||
server.run()
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
import "os" for Platform, Process
|
||||
import "io" for Directory
|
||||
|
||||
class Dashboard {
|
||||
static show() {
|
||||
System.print("========================================")
|
||||
System.print(" WREN CLI SYSTEM DASHBOARD ")
|
||||
System.print("========================================")
|
||||
|
||||
System.print("PLATFORM INFO:")
|
||||
System.print(" OS Name: %(Platform.name)")
|
||||
System.print(" POSIX: %(Platform.isPosix)")
|
||||
System.print(" Home: %(Platform.homePath)")
|
||||
|
||||
System.print("\nPROCESS INFO:")
|
||||
System.print(" PID: %(Process.pid)")
|
||||
System.print(" PPID: %(Process.ppid)")
|
||||
System.print(" CWD: %(Process.cwd)")
|
||||
System.print(" Version: %(Process.version)")
|
||||
|
||||
System.print("\nARGUMENTS:")
|
||||
if (Process.arguments.count == 0) {
|
||||
System.print(" (none)")
|
||||
} else {
|
||||
for (i in 0...Process.arguments.count) {
|
||||
System.print(" [%(i)]: %(Process.arguments[i])")
|
||||
}
|
||||
}
|
||||
|
||||
System.print("\nWORKING DIRECTORY FILES:")
|
||||
var files = Directory.list(".")
|
||||
files.sort(Fn.new {|a, b|
|
||||
var ba = a.bytes
|
||||
var bb = b.bytes
|
||||
var len = ba.count < bb.count ? ba.count : bb.count
|
||||
for (i in 0...len) {
|
||||
if (ba[i] < bb[i]) return true
|
||||
if (ba[i] > bb[i]) return false
|
||||
}
|
||||
return ba.count < bb.count
|
||||
})
|
||||
|
||||
var count = 0
|
||||
for (file in files) {
|
||||
if (count > 10) {
|
||||
System.print(" ... and %(files.count - 10) more")
|
||||
break
|
||||
}
|
||||
System.print(" - %(file)")
|
||||
count = count + 1
|
||||
}
|
||||
System.print("========================================")
|
||||
}
|
||||
}
|
||||
|
||||
Dashboard.show()
|
||||
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
import "scheduler" for Scheduler
|
||||
import "timer" for Timer
|
||||
|
||||
class TaskQueue {
|
||||
construct new() {
|
||||
_tasks = []
|
||||
_running = true
|
||||
}
|
||||
|
||||
add(name, duration) {
|
||||
System.print("[Queue] Adding task: \%(name) (\%(duration)ms)")
|
||||
_tasks.add({
|
||||
System.print("[Worker] Starting \%(name)...")
|
||||
Timer.sleep(duration)
|
||||
System.print("[Worker] Finished \%(name).")
|
||||
})
|
||||
}
|
||||
|
||||
start() {
|
||||
System.print("[Queue] Starting task runner...")
|
||||
Fiber.new {
|
||||
while (_running) {
|
||||
if (!_tasks.isEmpty) {
|
||||
var task = _tasks.removeAt(0)
|
||||
task.call()
|
||||
}
|
||||
Timer.sleep(100) // Yield to other fibers
|
||||
}
|
||||
}.call()
|
||||
}
|
||||
|
||||
stop() { _running = false }
|
||||
}
|
||||
|
||||
var queue = TaskQueue.new()
|
||||
|
||||
// Start the worker in the background
|
||||
queue.start()
|
||||
|
||||
// Add some tasks from the main thread
|
||||
queue.add("Task A", 1500)
|
||||
queue.add("Task B", 500)
|
||||
queue.add("Task C", 1000)
|
||||
|
||||
System.print("[Main] All tasks queued. Main fiber is free.")
|
||||
|
||||
// Keep the main loop alive for a bit to see the output
|
||||
Timer.sleep(4000)
|
||||
queue.stop()
|
||||
System.print("[Main] Shutdown.")
|
||||
Reference in New Issue
Block a user