18 Commits

Author SHA1 Message Date
John Alanbrook
957b964d9d better disrupt message on fd 2026-02-25 20:38:34 -06:00
John Alanbrook
fa9d2609b1 fix fd bug 2026-02-25 20:25:36 -06:00
John Alanbrook
e38c2f07bf Merge branch 'async_fd' 2026-02-25 17:43:12 -06:00
John Alanbrook
ecc1777b24 async cellfs 2026-02-25 17:43:01 -06:00
John Alanbrook
1cfd5b8133 add hot reload to util 2026-02-25 17:28:11 -06:00
John Alanbrook
9c1141f408 Merge branch 'async_http' 2026-02-25 16:39:16 -06:00
John Alanbrook
696cca530b internal pronto 2026-02-25 16:39:12 -06:00
John Alanbrook
c92a4087a6 Merge branch 'async_fd' 2026-02-25 16:24:20 -06:00
John Alanbrook
01637c49b0 fix sendmessage 2026-02-25 16:24:08 -06:00
John Alanbrook
f9e660ebaa better log 2026-02-25 16:07:39 -06:00
John Alanbrook
4f8fada57d Merge branch 'audit_build' 2026-02-25 16:06:17 -06:00
John Alanbrook
adcaa92bea better logging for compiling 2026-02-25 16:05:37 -06:00
John Alanbrook
fc36707b39 Merge branch 'async_fd' 2026-02-25 16:03:55 -06:00
John Alanbrook
7cb8ce7945 async fd 2026-02-25 16:03:52 -06:00
John Alanbrook
bb7997a751 fix sendmessage 2026-02-25 15:26:20 -06:00
John Alanbrook
327b990442 Merge branch 'master' into async_http 2026-02-25 14:48:46 -06:00
John Alanbrook
51c0a0b306 tls and http 2026-02-25 14:48:37 -06:00
John Alanbrook
8ac82016dd api for sending wota messages direct 2026-02-25 12:45:02 -06:00
22 changed files with 2066 additions and 496 deletions

View File

@@ -464,7 +464,7 @@ Build.compile_file = function(pkg, file, target, opts) {
// Compile
log.shop('compiling ' + file)
log.console('Compiling ' + file)
log.build('Compiling ' + file)
err_path = '/tmp/cell_build_err_' + content_hash(setup.src_path) + '.log'
full_cmd = setup.cmd_str + ' -o "' + obj_path + '" 2>"' + err_path + '"'
ret = os.system(full_cmd)
@@ -603,6 +603,8 @@ Build.build_module_dylib = function(pkg, file, target, opts) {
var post_probe = null
var fallback_probe = null
var _fail_msg2 = null
var link_err_path = null
var link_err_text = null
if (probe && probe.fail) {
_fail_msg2 = probe.fail_path ? text(fd.slurp(probe.fail_path)) : null
@@ -697,10 +699,16 @@ Build.build_module_dylib = function(pkg, file, target, opts) {
cmd_str = text(cmd_parts, ' ')
if (_opts.verbose) log.build('[verbose] link: ' + cmd_str)
log.shop('linking ' + file)
log.console('Linking module ' + file + ' -> ' + fd.basename(dylib_path))
ret = os.system(cmd_str)
log.build('Linking module ' + file + ' -> ' + fd.basename(dylib_path))
link_err_path = '/tmp/cell_link_err_' + content_hash(file) + '.log'
ret = os.system(cmd_str + ' 2>"' + link_err_path + '"')
if (ret != 0) {
log.error('Linking failed: ' + file)
if (fd.is_file(link_err_path))
link_err_text = text(fd.slurp(link_err_path))
if (link_err_text)
log.error('Linking failed: ' + file + '\n' + link_err_text)
else
log.error('Linking failed: ' + file)
return null
}
@@ -745,6 +753,9 @@ Build.build_dynamic = function(pkg, target, buildtype, opts) {
var _opts = opts || {}
var c_files = pkg_tools.get_c_files(pkg, _target, true)
var results = []
var total = length(c_files)
var done = 0
var failed = 0
// Pre-fetch cflags once to avoid repeated TOML reads
var pkg_dir = shop.get_package_dir(pkg)
@@ -760,14 +771,24 @@ Build.build_dynamic = function(pkg, target, buildtype, opts) {
})
}
if (total > 0)
os.print(' Building C modules ')
arrfor(c_files, function(file) {
var sym_name = shop.c_symbol_for_file(pkg, file)
var dylib = Build.build_module_dylib(pkg, file, _target, {buildtype: _buildtype, extra_objects: support_objects, cflags: cached_cflags, verbose: _opts.verbose, force: _opts.force})
if (dylib) {
push(results, {file: file, symbol: sym_name, dylib: dylib})
} else {
failed = failed + 1
}
done = done + 1
os.print('.')
})
if (total > 0)
os.print(` ${text(done)}/${text(total)}${failed > 0 ? `, ${text(failed)} failed` : ''}\n`)
// Write manifest so runtime can find dylibs without the build module
var mpath = manifest_path(pkg)
fd.slurpwrite(mpath, stone(blob(json.encode(results))))

164
cellfs.cm
View File

@@ -4,10 +4,11 @@ var fd = use('fd')
var miniz = use('miniz')
var qop = use('internal/qop')
var wildstar = use('internal/wildstar')
var blib = use('blob')
var mounts = []
var writepath = "."
var write_mount = null
function normalize_path(path) {
if (!path) return ""
@@ -30,7 +31,7 @@ function mount_exists(mount, path) {
result = mount.handle.stat(path) != null
} disruption {}
_check()
} else {
} else if (mount.type == 'fs') {
full_path = fd.join_paths(mount.source, path)
_check = function() {
st = fd.stat(full_path)
@@ -119,12 +120,12 @@ function resolve(path, must_exist) {
}
function mount(source, name) {
var st = fd.stat(source)
var st = null
var blob = null
var qop_archive = null
var zip = null
var _try_qop = null
var http = null
var mount_info = {
source: source,
@@ -134,6 +135,29 @@ function mount(source, name) {
zip_blob: null
}
if (starts_with(source, 'http://') || starts_with(source, 'https://')) {
http = use('http')
mount_info.type = 'http'
mount_info.handle = {
base_url: source,
get: function(path, callback) {
var url = source + '/' + path
$clock(function(_t) {
var resp = http.request('GET', url, null, null)
if (resp && resp.status == 200) {
callback(resp.body)
} else {
callback(null, "HTTP " + text(resp ? resp.status : 0) + ": " + url)
}
})
}
}
push(mounts, mount_info)
return
}
st = fd.stat(source)
if (st.isDirectory) {
mount_info.type = 'fs'
} else if (st.isFile) {
@@ -146,18 +170,18 @@ function mount(source, name) {
_try_qop()
if (qop_archive) {
mount_info.type = 'qop'
mount_info.handle = qop_archive
mount_info.zip_blob = blob
mount_info.type = 'qop'
mount_info.handle = qop_archive
mount_info.zip_blob = blob
} else {
zip = miniz.read(blob)
if (!is_object(zip) || !is_function(zip.count)) {
log.error("Invalid archive file (not zip or qop): " + source); disrupt
}
zip = miniz.read(blob)
if (!is_object(zip) || !is_function(zip.count)) {
log.error("Invalid archive file (not zip or qop): " + source); disrupt
}
mount_info.type = 'zip'
mount_info.handle = zip
mount_info.zip_blob = blob
mount_info.type = 'zip'
mount_info.handle = zip
mount_info.zip_blob = blob
}
} else {
log.error("Unsupported mount source type: " + source); disrupt
@@ -191,11 +215,13 @@ function slurp(path) {
}
function slurpwrite(path, data) {
var full_path = writepath + "/" + path
var f = fd.open(full_path, 'w')
fd.write(f, data)
fd.close(f)
var full_path = null
if (write_mount) {
full_path = fd.join_paths(write_mount.source, path)
} else {
full_path = fd.join_paths(".", path)
}
fd.slurpwrite(full_path, data)
}
function exists(path) {
@@ -276,12 +302,25 @@ function rm(path) {
}
function mkdir(path) {
var full = fd.join_paths(writepath, path)
var full = null
if (write_mount) {
full = fd.join_paths(write_mount.source, path)
} else {
full = fd.join_paths(".", path)
}
fd.mkdir(full)
}
function set_writepath(path) {
writepath = path
function set_writepath(mount_name) {
var found = null
if (mount_name == null) { write_mount = null; return }
arrfor(mounts, function(m) {
if (m.name == mount_name) { found = m; return true }
}, false, true)
if (!found || found.type != 'fs') {
log.error("writepath: must be an fs mount"); disrupt
}
write_mount = found
}
function basedir() {
@@ -449,6 +488,82 @@ function globfs(globs, _dir) {
return results
}
// Requestor factory: returns a requestor for reading a file at path
function get(path) {
return function get_requestor(callback, value) {
var res = resolve(path, false)
var full = null
var f = null
var acc = null
var cancelled = false
var data = null
var _close = null
if (!res) { callback(null, "not found: " + path); return }
if (res.mount.type == 'zip') {
callback(res.mount.handle.slurp(res.path))
return
}
if (res.mount.type == 'qop') {
data = res.mount.handle.read(res.path)
if (data) {
callback(data)
} else {
callback(null, "not found in qop: " + path)
}
return
}
if (res.mount.type == 'http') {
res.mount.handle.get(res.path, callback)
return
}
full = fd.join_paths(res.mount.source, res.path)
f = fd.open(full, 'r')
acc = blob()
function next(_t) {
var chunk = null
if (cancelled) return
chunk = fd.read(f, 65536)
if (length(chunk) == 0) {
fd.close(f)
stone(acc)
callback(acc)
return
}
acc.write_blob(chunk)
$clock(next)
}
next()
return function cancel() {
cancelled = true
_close = function() { fd.close(f) } disruption {}
_close()
}
}
}
// Requestor factory: returns a requestor for writing data to path
function put(path, data) {
return function put_requestor(callback, value) {
var _data = data != null ? data : value
var full = null
var _do = null
if (!write_mount) { callback(null, "no write mount set"); return }
full = fd.join_paths(write_mount.source, path)
_do = function() {
fd.slurpwrite(full, _data)
callback(true)
} disruption {
callback(null, "write failed: " + path)
}
_do()
}
}
cellfs.mount = mount
cellfs.mount_package = mount_package
cellfs.unmount = unmount
@@ -467,7 +582,8 @@ cellfs.writepath = set_writepath
cellfs.basedir = basedir
cellfs.prefdir = prefdir
cellfs.realdir = realdir
cellfs.mount('.')
cellfs.get = get
cellfs.put = put
cellfs.resolve = resolve
return cellfs

View File

@@ -9,13 +9,16 @@ Logging in ƿit is channel-based. Any `log.X(value)` call writes to channel `"X"
## Channels
Three channels are conventional:
These channels are conventional:
| Channel | Usage |
|---------|-------|
| `log.console(msg)` | General output |
| `log.error(msg)` | Errors and warnings |
| `log.error(msg)` | Errors |
| `log.warn(msg)` | Compiler diagnostics and warnings |
| `log.system(msg)` | Internal system messages |
| `log.build(msg)` | Per-file compile/link output |
| `log.shop(msg)` | Package management internals |
Any name works. `log.debug(msg)` creates channel `"debug"`, `log.perf(msg)` creates `"perf"`, and so on.
@@ -29,16 +32,18 @@ Non-text values are JSON-encoded automatically.
## Default Behavior
With no configuration, a default sink routes `console`, `error`, and `system` to the terminal in pretty format. The `error` channel includes a stack trace by default:
With no configuration, a default sink routes `console` and `error` to the terminal in clean format. The `error` channel includes a stack trace by default:
```
[a3f12] [console] server started on port 8080
[a3f12] [error] connection refused
server started on port 8080
error: connection refused
at handle_request (server.ce:42:3)
at main (main.ce:5:1)
```
The format is `[actor_id] [channel] message`. Error stack traces are always on unless you explicitly configure a sink without them.
Clean format prints messages without actor ID or channel prefix. Error messages are prefixed with `error:`. Other formats (`pretty`, `bare`) include actor IDs and are available for debugging. Stack traces are always on for errors unless you explicitly configure a sink without them.
Channels like `warn`, `build`, and `shop` are not routed to the terminal by default. Enable them with `pit log enable <channel>`.
## Configuration
@@ -67,7 +72,7 @@ exclude = ["console"]
| Field | Values | Description |
|-------|--------|-------------|
| `type` | `"console"`, `"file"` | Where output goes |
| `format` | `"pretty"`, `"bare"`, `"json"` | How output is formatted |
| `format` | `"clean"`, `"pretty"`, `"bare"`, `"json"` | How output is formatted |
| `channels` | array of names, or `["*"]` | Which channels this sink receives. Quote `'*'` on the CLI to prevent shell glob expansion. |
| `exclude` | array of names | Channels to skip (useful with `"*"`) |
| `stack` | array of channel names | Channels that capture a stack trace |
@@ -75,6 +80,13 @@ exclude = ["console"]
### Formats
**clean** — CLI-friendly. No actor ID or channel prefix. Error channel messages are prefixed with `error:`. This is the default format.
```
server started on port 8080
error: connection refused
```
**pretty** — human-readable, one line per message. Includes actor ID, channel, source location, and message.
```
@@ -158,7 +170,10 @@ The `pit log` command manages sinks and reads log files. See [CLI — pit log](/
```bash
pit log list # show sinks
pit log add terminal console --format=bare --channels=console
pit log channels # list channels with enabled/disabled status
pit log enable warn # enable a channel on the terminal sink
pit log disable warn # disable a channel
pit log add terminal console --format=clean --channels=console
pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console
pit log add debug console --channels=error,debug --stack=error,debug
pit log remove terminal
@@ -166,6 +181,16 @@ pit log read dump --lines=20 --channel=error
pit log tail dump
```
### Channel toggling
The `enable` and `disable` commands modify the terminal sink's channel list without touching other sink configuration. This is the easiest way to opt in to extra output:
```bash
pit log enable warn # see compiler warnings
pit log enable build # see per-file compile/link output
pit log disable warn # hide warnings again
```
## Examples
### Development setup

515
http.cm
View File

@@ -1,14 +1,20 @@
var socket = use('socket')
var c_http = use('net/http')
var tls = use('net/tls')
var Blob = use('blob')
def CRLF = "\r\n"
def status_texts = {
"200": "OK", "201": "Created", "204": "No Content",
"301": "Moved Permanently", "302": "Found", "307": "Temporary Redirect",
"400": "Bad Request", "401": "Unauthorized", "403": "Forbidden",
"404": "Not Found", "405": "Method Not Allowed", "500": "Internal Server Error"
}
// ============================================================
// Server (unchanged)
// ============================================================
function serve(port) {
var fd = socket.socket("AF_INET", "SOCK_STREAM")
socket.setsockopt(fd, "SOL_SOCKET", "SO_REUSEADDR", true)
@@ -152,6 +158,10 @@ function sse_close(conn) {
socket.close(conn)
}
// ============================================================
// Blocking client request (kept for compatibility)
// ============================================================
function request(method, url, headers, body) {
var parts = array(url, "/")
var host_port = parts[2]
@@ -221,13 +231,512 @@ function request(method, url, headers, body) {
}
}
// ============================================================
// Requestor-based async fetch
// ============================================================
// parse_url requestor — sync, extract {scheme, host, port, path} from URL
var parse_url = function(callback, value) {
var url = null
var method = "GET"
var req_headers = null
var req_body = null
log.console("value type=" + text(is_text(value)) + " val=" + text(value))
if (is_text(value)) {
url = value
log.console("url after assign=" + text(is_text(url)) + " url=" + text(url))
} else {
url = value.url
if (value.method != null) method = value.method
if (value.headers != null) req_headers = value.headers
if (value.body != null) req_body = value.body
}
// strip scheme
var scheme = "http"
var rest = url
var scheme_end = search(url, "://")
log.console("A: url_type=" + text(is_text(url)) + " scheme_end=" + text(scheme_end))
if (scheme_end != null) {
scheme = lower(text(url, 0, scheme_end))
rest = text(url, scheme_end + 3, length(url))
log.console("B: scheme=" + scheme + " rest=" + rest + " rest_type=" + text(is_text(rest)))
}
// split host from path
var slash = search(rest, "/")
var host_port = rest
var path = "/"
log.console("C: slash=" + text(slash))
if (slash != null) {
host_port = text(rest, 0, slash)
path = text(rest, slash, length(rest))
}
// split host:port
var hp = array(host_port, ":")
var host = hp[0]
var port = null
if (length(hp) > 1) {
port = number(hp[1])
} else {
port = scheme == "https" ? 443 : 80
}
callback({
scheme: scheme, host: host, port: port, path: path,
host_port: host_port, method: method,
req_headers: req_headers, req_body: req_body
})
return null
}
// resolve_dns requestor — blocking getaddrinfo, swappable later
var resolve_dns = function(callback, state) {
var ok = true
var addrs = null
var _resolve = function() {
addrs = socket.getaddrinfo(state.host, text(state.port))
} disruption {
ok = false
}
_resolve()
if (!ok || addrs == null || length(addrs) == 0) {
callback(null, "dns resolution failed for " + state.host)
return null
}
callback(record(state, {address: addrs[0].address}))
return null
}
// open_connection requestor — non-blocking connect + optional TLS
var open_connection = function(callback, state) {
var fd = socket.socket("AF_INET", "SOCK_STREAM")
var cancelled = false
var cancel = function() {
var _close = null
if (!cancelled) {
cancelled = true
_close = function() {
socket.unwatch(fd)
socket.close(fd)
} disruption {}
_close()
}
}
socket.setnonblock(fd)
var finish_connect = function(the_fd) {
var ctx = null
if (state.scheme == "https") {
ctx = tls.wrap(the_fd, state.host)
}
callback(record(state, {fd: the_fd, tls: ctx}))
}
// non-blocking connect — EINPROGRESS is expected
var connect_err = false
var _connect = function() {
socket.connect(fd, {address: state.address, port: state.port})
} disruption {
connect_err = true
}
_connect()
// if connect succeeded immediately (localhost, etc)
var _finish_immediate = null
if (!connect_err && !cancelled) {
_finish_immediate = function() {
finish_connect(fd)
} disruption {
cancel()
callback(null, "connection setup failed")
}
_finish_immediate()
return cancel
}
// wait for connect to complete
socket.on_writable(fd, function() {
if (cancelled) return
var err = socket.getsockopt(fd, "SOL_SOCKET", "SO_ERROR")
if (err != 0) {
cancel()
callback(null, "connect failed (errno " + text(err) + ")")
return
}
var _finish = function() {
finish_connect(fd)
} disruption {
cancel()
callback(null, "connection setup failed")
}
_finish()
})
return cancel
}
// send_request requestor — format + send HTTP/1.1 request
var send_request = function(callback, state) {
var cancelled = false
var cancel = function() {
cancelled = true
}
var _send = function() {
var body_str = ""
var keys = null
var i = 0
if (state.req_body != null) {
if (is_text(state.req_body)) body_str = state.req_body
else body_str = text(state.req_body)
}
var req = state.method + " " + state.path + " HTTP/1.1" + CRLF
req = req + "Host: " + state.host_port + CRLF
req = req + "Connection: close" + CRLF
req = req + "User-Agent: cell/1.0" + CRLF
req = req + "Accept: */*" + CRLF
if (state.req_headers != null) {
keys = array(state.req_headers)
i = 0
while (i < length(keys)) {
req = req + keys[i] + ": " + state.req_headers[keys[i]] + CRLF
i = i + 1
}
}
if (length(body_str) > 0) {
req = req + "Content-Length: " + text(length(body_str)) + CRLF
}
req = req + CRLF + body_str
if (state.tls != null) {
tls.send(state.tls, req)
} else {
socket.send(state.fd, req)
}
} disruption {
if (!cancelled) callback(null, "send request failed")
return cancel
}
_send()
if (!cancelled) callback(state)
return cancel
}
// parse response headers from raw text
function parse_headers(raw) {
var hdr_end = search(raw, CRLF + CRLF)
if (hdr_end == null) return null
var header_text = text(raw, 0, hdr_end)
var lines = array(header_text, CRLF)
var status_parts = array(lines[0], " ")
var status_code = number(status_parts[1])
var headers = {}
var i = 1
var colon = null
while (i < length(lines)) {
colon = search(lines[i], ": ")
if (colon != null) {
headers[lower(text(lines[i], 0, colon))] = text(lines[i], colon + 2)
}
i = i + 1
}
return {
status: status_code, headers: headers,
body_start: hdr_end + 4
}
}
// decode chunked transfer encoding
function decode_chunked(body_text) {
var result = ""
var pos = 0
var chunk_end = null
var chunk_size = null
while (pos < length(body_text)) {
chunk_end = search(text(body_text, pos), CRLF)
if (chunk_end == null) return result
chunk_size = number(text(body_text, pos, pos + chunk_end), 16)
if (chunk_size == null || chunk_size == 0) return result
pos = pos + chunk_end + 2
result = result + text(body_text, pos, pos + chunk_size)
pos = pos + chunk_size + 2
}
return result
}
// receive_response requestor — async incremental receive
var receive_response = function(callback, state) {
var cancelled = false
var buffer = ""
var parsed = null
var content_length = null
var is_chunked = false
var body_complete = false
var cancel = function() {
var _cleanup = null
if (!cancelled) {
cancelled = true
_cleanup = function() {
if (state.tls != null) {
tls.close(state.tls)
} else {
socket.unwatch(state.fd)
socket.close(state.fd)
}
} disruption {}
_cleanup()
}
}
var finish = function() {
if (cancelled) return
var body_text = text(buffer, parsed.body_start)
if (is_chunked) {
body_text = decode_chunked(body_text)
}
// clean up connection
var _cleanup = function() {
if (state.tls != null) {
tls.close(state.tls)
} else {
socket.close(state.fd)
}
} disruption {}
_cleanup()
callback({
status: parsed.status,
headers: parsed.headers,
body: body_text
})
}
var check_complete = function() {
var te = null
var cl = null
var body_text = null
// still waiting for headers
if (parsed == null) {
parsed = parse_headers(buffer)
if (parsed == null) return false
te = parsed.headers["transfer-encoding"]
if (te != null && search(lower(te), "chunked") != null) {
is_chunked = true
}
cl = parsed.headers["content-length"]
if (cl != null) content_length = number(cl)
}
body_text = text(buffer, parsed.body_start)
if (is_chunked) {
// chunked: look for the terminating 0-length chunk
if (search(body_text, CRLF + "0" + CRLF) != null) return true
if (starts_with(body_text, "0" + CRLF)) return true
return false
}
if (content_length != null) {
return length(body_text) >= content_length
}
// connection: close — we read until EOF (handled by recv returning 0 bytes)
return false
}
var on_data = function() {
if (cancelled) return
var chunk = null
var got_data = false
var eof = false
var _recv = function() {
if (state.tls != null) {
chunk = tls.recv(state.tls, 16384)
} else {
chunk = socket.recv(state.fd, 16384)
}
} disruption {
// recv error — treat as EOF
eof = true
}
_recv()
var chunk_text = null
if (!eof && chunk != null) {
stone(chunk)
chunk_text = text(chunk)
if (length(chunk_text) > 0) {
buffer = buffer + chunk_text
got_data = true
} else {
eof = true
}
}
if (got_data && check_complete()) {
finish()
return
}
if (eof) {
// connection closed — if we have headers, deliver what we have
if (parsed != null || parse_headers(buffer) != null) {
if (parsed == null) parsed = parse_headers(buffer)
finish()
} else {
cancel()
callback(null, "connection closed before headers received")
}
return
}
// re-register for more data (one-shot watches)
if (!cancelled) {
if (state.tls != null) {
tls.on_readable(state.tls, on_data)
} else {
socket.on_readable(state.fd, on_data)
}
}
}
// start reading
if (state.tls != null) {
tls.on_readable(state.tls, on_data)
} else {
socket.on_readable(state.fd, on_data)
}
return cancel
}
// ============================================================
// fetch — synchronous HTTP(S) GET, returns response body (stoned blob)
// ============================================================
var fetch = function(url) {
var scheme = "http"
var rest = url
var scheme_end = search(url, "://")
var slash = null
var host_port = null
var path = "/"
var hp = null
var host = null
var port = null
var fd = null
var ctx = null
var buf = Blob()
var raw_text = null
var hdr_end = null
var header_text = null
var body_start_bits = null
var body = null
var addrs = null
var address = null
var ok = true
if (scheme_end != null) {
scheme = lower(text(url, 0, scheme_end))
rest = text(url, scheme_end + 3, length(url))
}
slash = search(rest, "/")
host_port = rest
if (slash != null) {
host_port = text(rest, 0, slash)
path = text(rest, slash, length(rest))
}
hp = array(host_port, ":")
host = hp[0]
port = length(hp) > 1 ? number(hp[1]) : (scheme == "https" ? 443 : 80)
addrs = socket.getaddrinfo(host, text(port))
if (addrs == null || length(addrs) == 0) return null
address = addrs[0].address
fd = socket.socket("AF_INET", "SOCK_STREAM")
var _do = function() {
var req = null
var chunk = null
socket.connect(fd, {address: address, port: port})
if (scheme == "https") ctx = tls.wrap(fd, host)
req = "GET " + path + " HTTP/1.1" + CRLF
req = req + "Host: " + host_port + CRLF
req = req + "Connection: close" + CRLF
req = req + "User-Agent: cell/1.0" + CRLF
req = req + "Accept: */*" + CRLF + CRLF
if (ctx != null) tls.send(ctx, req)
else socket.send(fd, req)
while (true) {
if (ctx != null) chunk = tls.recv(ctx, 16384)
else chunk = socket.recv(fd, 16384)
if (chunk == null) break
stone(chunk)
if (length(chunk) == 0) break
buf.write_blob(chunk)
}
} disruption {
ok = false
}
_do()
var _cleanup = function() {
if (ctx != null) tls.close(ctx)
else socket.close(fd)
} disruption {}
_cleanup()
if (!ok) return null
stone(buf)
raw_text = text(buf)
hdr_end = search(raw_text, CRLF + CRLF)
if (hdr_end == null) return null
header_text = text(raw_text, 0, hdr_end)
if (search(lower(header_text), "transfer-encoding: chunked") != null)
return decode_chunked(text(raw_text, hdr_end + 4))
// Headers are ASCII so char offset = byte offset
body_start_bits = (hdr_end + 4) * 8
body = buf.read_blob(body_start_bits, length(buf))
stone(body)
return body
}
// ============================================================
// fetch_requestor — async requestor pipeline for fetch
// ============================================================
var fetch_requestor = sequence([
parse_url,
resolve_dns,
open_connection,
send_request,
receive_response
])
function close(fd) {
socket.close(fd)
}
return {
// server
serve: serve, accept: accept, on_request: on_request,
respond: respond, request: request,
respond: respond, close: close,
sse_open: sse_open, sse_event: sse_event, sse_close: sse_close,
close: close, fetch: c_http.fetch
// client
fetch: fetch,
fetch_requestor: fetch_requestor,
request: request
}

View File

@@ -259,7 +259,6 @@ if (_native) {
compile_native_cached(_te.name, core_path + '/' + _te.path)
_ti = _ti + 1
}
os.print("bootstrap: native cache seeded\n")
} else {
// Bytecode path: seed cache with everything engine needs
_targets = [
@@ -276,5 +275,4 @@ if (_native) {
compile_and_cache(_te.name, core_path + '/' + _te.path)
_ti = _ti + 1
}
os.print("bootstrap: cache seeded\n")
}

View File

@@ -285,7 +285,7 @@ function analyze(src, filename) {
_i = 0
while (_i < length(folded._diagnostics)) {
e = folded._diagnostics[_i]
os.print(`${filename}:${text(e.line)}:${text(e.col)}: ${e.severity}: ${e.message}\n`)
log.warn(`${filename}:${text(e.line)}:${text(e.col)}: ${e.severity}: ${e.message}`)
_i = _i + 1
}
if (_wm) {
@@ -443,8 +443,12 @@ function run_ast_fn(name, ast, env, pkg) {
_has_errors = false
while (_di < length(optimized._diagnostics)) {
_diag = optimized._diagnostics[_di]
os.print(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}\n`)
if (_diag.severity == "error") _has_errors = true
if (_diag.severity == "error") {
log.error(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}`)
_has_errors = true
} else {
log.warn(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}`)
}
_di = _di + 1
}
if (_has_errors) disrupt
@@ -506,8 +510,12 @@ function compile_user_blob(name, ast, pkg) {
_has_errors = false
while (_di < length(optimized._diagnostics)) {
_diag = optimized._diagnostics[_di]
os.print(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}\n`)
if (_diag.severity == "error") _has_errors = true
if (_diag.severity == "error") {
log.error(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}`)
_has_errors = true
} else {
log.warn(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}`)
}
_di = _di + 1
}
if (_has_errors) disrupt
@@ -812,11 +820,29 @@ function create_actor(desc) {
var $_ = {}
// Forward-declare actor system variables so closures in $_ can capture them.
// Initialized here; values used at runtime when fully set up.
var time = null
var enet = null
var HEADER = {}
var underlings = {}
var overling = null
var root = null
var receive_fn = null
var greeters = {}
var message_queue = []
var couplings = {}
var peers = {}
var id_address = {}
var peer_queue = {}
var portal = null
var portal_fn = null
var replies = {}
use_cache['core/json'] = json
// Create runtime_env early (empty) -- filled after pronto loads.
// Shop accesses it lazily (in inject_env, called at module-use time, not load time)
// so it sees the filled version.
// Runtime env: passed to package modules via shop's inject_env.
// Requestor functions are added immediately below; actor/log/send added later.
var runtime_env = {}
// Populate core_extras with everything shop (and other core modules) need
@@ -840,6 +866,217 @@ core_extras.ensure_build_dir = ensure_build_dir
core_extras.compile_to_blob = compile_to_blob
core_extras.native_mode = native_mode
// Load pronto early so requestor functions (sequence, parallel, etc.) are
// available to core modules loaded below (http, shop, etc.)
var pronto = use_core('internal/pronto')
var fallback = pronto.fallback
var parallel = pronto.parallel
var race = pronto.race
var sequence = pronto.sequence
core_extras.fallback = fallback
core_extras.parallel = parallel
core_extras.race = race
core_extras.sequence = sequence
// Set actor identity before shop loads so $self is available
if (!_cell.args.id) _cell.id = guid()
else _cell.id = _cell.args.id
$_.self = create_actor({id: _cell.id})
overling = _cell.args.overling_id ? create_actor({id: _cell.args.overling_id}) : null
$_.overling = overling
root = _cell.args.root_id ? create_actor({id: _cell.args.root_id}) : null
if (root == null) root = $_.self
// Define all actor intrinsic functions ($clock, $delay, etc.) before shop loads.
// Closures here capture module-level variables by reference; those variables
// are fully initialized before these functions are ever called at runtime.
$_.clock = function(fn) {
actor_mod.clock(_ => {
fn(time.number())
send_messages()
})
}
$_.delay = function delay(fn, seconds) {
var _seconds = seconds == null ? 0 : seconds
function delay_turn() {
fn()
send_messages()
}
var id = actor_mod.delay(delay_turn, _seconds)
return function() { actor_mod.removetimer(id) }
}
$_.stop = function stop(actor) {
if (!actor) {
need_stop = true
return
}
if (!is_actor(actor)) {
log.error('Can only call stop on an actor.')
disrupt
}
if (is_null(underlings[actor[ACTORDATA].id])) {
log.error('Can only call stop on an underling or self.')
disrupt
}
sys_msg(actor, {kind:"stop"})
}
$_.start = function start(cb, program) {
if (!program) return
var id = guid()
var oid = $_.self[ACTORDATA].id
var startup = {
id,
overling_id: oid,
root_id: root ? root[ACTORDATA].id : null,
program,
native_mode: native_mode,
no_warn: _no_warn,
}
greeters[id] = cb
push(message_queue, { startup })
}
$_.receiver = function receiver(fn) {
receive_fn = fn
}
$_.unneeded = function unneeded(fn, seconds) {
actor_mod.unneeded(fn, seconds)
}
$_.couple = function couple(actor) {
if (actor == $_.self) return
couplings[actor[ACTORDATA].id] = true
sys_msg(actor, {kind:'couple', from_id: _cell.id})
log.system(`coupled to ${actor[ACTORDATA].id}`)
}
$_.contact = function(callback, record) {
send(create_actor(record), record, callback)
}
$_.portal = function(fn, port) {
if (portal) {
log.error(`Already started a portal listening on ${portal.port()}`)
disrupt
}
if (!port) {
log.error("Requires a valid port.")
disrupt
}
log.system(`starting a portal on port ${port}`)
portal = enet.create_host({address: "any", port})
portal_fn = fn
enet_check()
}
$_.connection = function(callback, actor, config) {
var peer = peers[actor[ACTORDATA].id]
if (peer) {
callback(peer_connection(peer))
return
}
if (actor_mod.mailbox_exist(actor[ACTORDATA].id)) {
callback({type:"local"})
return
}
callback()
}
$_.time_limit = function(requestor, seconds) {
if (!pronto.is_requestor(requestor)) {
log.error('time_limit: first argument must be a requestor')
disrupt
}
if (!is_number(seconds) || seconds <= 0) {
log.error('time_limit: seconds must be a positive number')
disrupt
}
return function time_limit_requestor(callback, value) {
pronto.check_callback(callback, 'time_limit')
var finished = false
var requestor_cancel = null
var timer_cancel = null
function cancel(reason) {
if (finished) return
finished = true
if (timer_cancel) {
timer_cancel()
timer_cancel = null
}
if (requestor_cancel) {
requestor_cancel(reason)
requestor_cancel = null
}
}
function safe_cancel_requestor(reason) {
if (requestor_cancel) {
requestor_cancel(reason)
requestor_cancel = null
}
}
timer_cancel = $_.delay(function() {
if (finished) return
def reason = {
factory: time_limit_requestor,
excuse: 'Timeout.',
evidence: seconds,
message: 'Timeout. ' + text(seconds)
}
safe_cancel_requestor(reason)
finished = true
callback(null, reason)
}, seconds)
function do_request() {
requestor_cancel = requestor(function(val, reason) {
if (finished) return
finished = true
if (timer_cancel) {
timer_cancel()
timer_cancel = null
}
callback(val, reason)
}, value)
} disruption {
cancel('requestor failed')
callback(null, 'requestor failed')
}
do_request()
return function(reason) {
safe_cancel_requestor(reason)
}
}
}
// Make actor intrinsics available to core modules loaded via use_core
core_extras['$self'] = $_.self
core_extras['$overling'] = $_.overling
core_extras['$clock'] = $_.clock
core_extras['$delay'] = $_.delay
core_extras['$start'] = $_.start
core_extras['$stop'] = $_.stop
core_extras['$receiver'] = $_.receiver
core_extras['$contact'] = $_.contact
core_extras['$portal'] = $_.portal
core_extras['$time_limit'] = $_.time_limit
core_extras['$couple'] = $_.couple
core_extras['$unneeded'] = $_.unneeded
core_extras['$connection'] = $_.connection
core_extras['$fd'] = fd
// NOW load shop -- it receives all of the above via env
var shop = use_core('internal/shop')
use_core('build')
@@ -859,7 +1096,7 @@ _summary_resolver = function(path, ctx) {
return summary_fn()
}
var time = use_core('time')
time = use_core('time')
var toml = use_core('toml')
// --- Logging system (full version) ---
@@ -892,7 +1129,11 @@ function build_sink_routing() {
if (!is_array(sink.exclude)) sink.exclude = []
if (is_text(sink.stack)) sink.stack = [sink.stack]
if (!is_array(sink.stack)) sink.stack = []
if (sink.type == "file" && sink.path) ensure_log_dir(sink.path)
if (sink.type == "file" && sink.path) {
ensure_log_dir(sink.path)
if (sink.mode == "overwrite")
fd.slurpwrite(sink.path, stone(_make_blob("")))
}
arrfor(sink.stack, function(ch) {
stack_channels[ch] = true
})
@@ -920,15 +1161,20 @@ function load_log_config() {
sink: {
terminal: {
type: "console",
format: "pretty",
channels: ["*"],
exclude: ["system", "shop", "build"],
format: "clean",
channels: ["console", "error"],
stack: ["error"]
}
}
}
}
build_sink_routing()
var names = array(log_config.sink)
arrfor(names, function(name) {
var sink = log_config.sink[name]
if (sink.type == "file")
os.print("[log] " + name + " -> " + sink.path + "\n")
})
}
function pretty_format(rec) {
@@ -964,6 +1210,25 @@ function bare_format(rec) {
return out
}
function clean_format(rec) {
var ev = is_text(rec.event) ? rec.event : json.encode(rec.event, false)
var out = null
var i = 0
var fr = null
if (rec.channel == "error") {
out = `error: ${ev}\n`
} else {
out = `${ev}\n`
}
if (rec.stack && length(rec.stack) > 0) {
for (i = 0; i < length(rec.stack); i = i + 1) {
fr = rec.stack[i]
out = out + ` at ${fr.fn} (${fr.file}:${text(fr.line)}:${text(fr.col)})\n`
}
}
return out
}
function sink_excluded(sink, channel) {
var excluded = false
if (!sink.exclude || length(sink.exclude) == 0) return false
@@ -975,17 +1240,25 @@ function sink_excluded(sink, channel) {
function dispatch_to_sink(sink, rec) {
var line = null
var st = null
if (sink_excluded(sink, rec.channel)) return
if (sink.type == "console") {
if (sink.format == "json")
os.print(json.encode(rec, false) + "\n")
else if (sink.format == "bare")
os.print(bare_format(rec))
else if (sink.format == "clean")
os.print(clean_format(rec))
else
os.print(pretty_format(rec))
} else if (sink.type == "file") {
line = json.encode(rec, false) + "\n"
fd.slurpappend(sink.path, stone(blob(line)))
if (sink.max_size) {
st = fd.stat(sink.path)
if (st && st.size > sink.max_size)
fd.slurpwrite(sink.path, stone(_make_blob("")))
}
fd.slurpappend(sink.path, stone(_make_blob(line)))
}
}
@@ -1030,12 +1303,6 @@ actor_mod.set_log(log)
// (before the full log was ready) captured the bootstrap function reference.
_log_full = log
var pronto = use_core('pronto')
var fallback = pronto.fallback
var parallel = pronto.parallel
var race = pronto.race
var sequence = pronto.sequence
runtime_env.actor = actor
runtime_env.log = log
runtime_env.send = send
@@ -1048,73 +1315,6 @@ runtime_env.sequence = sequence
// Make runtime functions available to modules loaded via use_core
arrfor(array(runtime_env), function(k) { core_extras[k] = runtime_env[k] })
$_.time_limit = function(requestor, seconds)
{
if (!pronto.is_requestor(requestor)) {
log.error('time_limit: first argument must be a requestor')
disrupt
}
if (!is_number(seconds) || seconds <= 0) {
log.error('time_limit: seconds must be a positive number')
disrupt
}
return function time_limit_requestor(callback, value) {
pronto.check_callback(callback, 'time_limit')
var finished = false
var requestor_cancel = null
var timer_cancel = null
function cancel(reason) {
if (finished) return
finished = true
if (timer_cancel) {
timer_cancel()
timer_cancel = null
}
if (requestor_cancel) {
requestor_cancel(reason)
requestor_cancel = null
}
}
function safe_cancel_requestor(reason) {
if (requestor_cancel) {
requestor_cancel(reason)
requestor_cancel = null
}
}
timer_cancel = $_.delay(function() {
if (finished) return
def reason = make_reason(factory, 'Timeout.', seconds)
safe_cancel_requestor(reason)
finished = true
callback(null, reason)
}, seconds)
function do_request() {
requestor_cancel = requestor(function(val, reason) {
if (finished) return
finished = true
if (timer_cancel) {
timer_cancel()
timer_cancel = null
}
callback(val, reason)
}, value)
} disruption {
cancel('requestor failed')
callback(null, 'requestor failed')
}
do_request()
return function(reason) {
safe_cancel_requestor(reason)
}
}
}
var config = {
ar_timer: 60,
actor_memory:0,
@@ -1161,31 +1361,8 @@ function guid(bits)
return text(guid,'h')
}
var HEADER = {}
// takes a function input value that will eventually be called with the current time in number form.
$_.clock = function(fn) {
actor_mod.clock(_ => {
fn(time.number())
send_messages()
})
}
var underlings = {} // this is more like "all actors that are notified when we die"
var overling = null
var root = null
var receive_fn = null
var greeters = {} // Router functions for when messages are received for a specific actor
var enet = use_core('internal/enet')
var peers = {}
var id_address = {}
var peer_queue = {}
var portal = null
var portal_fn = null
var enet = use_core('enet')
enet = use_core('internal/enet')
enet = use_core('enet')
function peer_connection(peer) {
return {
@@ -1210,37 +1387,6 @@ function peer_connection(peer) {
}
}
// takes a callback function, an actor object, and a configuration record for getting information about the status of a connection to the actor. The configuration record is used to request the sort of information that needs to be communicated. This can include latency, bandwidth, activity, congestion, cost, partitions. The callback is given a record containing the requested information.
$_.connection = function(callback, actor, config) {
var peer = peers[actor[ACTORDATA].id]
if (peer) {
callback(peer_connection(peer))
return
}
if (actor_mod.mailbox_exist(actor[ACTORDATA].id)) {
callback({type:"local"})
return
}
callback()
}
// takes a function input value that will eventually be called with the current time in number form.
$_.portal = function(fn, port) {
if (portal) {
log.error(`Already started a portal listening on ${portal.port()}`)
disrupt
}
if (!port) {
log.error("Requires a valid port.")
disrupt
}
log.system(`starting a portal on port ${port}`)
portal = enet.create_host({address: "any", port})
portal_fn = fn
enet_check()
}
function handle_host(e) {
var queue = null
var data = null
@@ -1288,78 +1434,6 @@ function populate_actor_addresses(obj, e) {
})
}
// takes a callback function, an actor object, and a configuration record for getting information about the status of a connection to the actor. The configuration record is used to request the sort of information that needs to be communicated. This can include latency, bandwidth, activity, congestion, cost, partitions. The callback is given a record containing the requested information.
$_.contact = function(callback, record) {
send(create_actor(record), record, callback)
}
// registers a function that will receive all messages...
$_.receiver = function receiver(fn) {
receive_fn = fn
}
// Holds all messages queued during the current turn.
var message_queue = []
$_.start = function start(cb, program) {
if (!program) return
var id = guid()
var oid = $_.self[ACTORDATA].id
var startup = {
id,
overling_id: oid,
root_id: root ? root[ACTORDATA].id : null,
program,
native_mode: native_mode,
no_warn: _no_warn,
}
greeters[id] = cb
push(message_queue, { startup })
}
// stops an underling or self.
$_.stop = function stop(actor) {
if (!actor) {
need_stop = true
return
}
if (!is_actor(actor)) {
log.error('Can only call stop on an actor.')
disrupt
}
if (is_null(underlings[actor[ACTORDATA].id])) {
log.error('Can only call stop on an underling or self.')
disrupt
}
sys_msg(actor, {kind:"stop"})
}
// schedules the removal of an actor after a specified amount of time.
$_.unneeded = function unneeded(fn, seconds) {
actor_mod.unneeded(fn, seconds)
}
// schedules the invocation of a function after a specified amount of time.
$_.delay = function delay(fn, seconds) {
var _seconds = seconds == null ? 0 : seconds
function delay_turn() {
fn()
send_messages()
}
var id = actor_mod.delay(delay_turn, _seconds)
return function() { actor_mod.removetimer(id) }
}
// causes this actor to stop when another actor stops.
var couplings = {}
$_.couple = function couple(actor) {
if (actor == $_.self) return // can't couple to self
couplings[actor[ACTORDATA].id] = true
sys_msg(actor, {kind:'couple', from_id: _cell.id})
log.system(`coupled to ${actor[ACTORDATA].id}`)
}
function actor_prep(actor, send) {
push(message_queue, {actor,send});
@@ -1448,8 +1522,6 @@ function send_messages() {
message_queue = []
}
var replies = {}
function send(actor, message, reply) {
var send_msg = null
var target = null
@@ -1498,11 +1570,6 @@ function send(actor, message, reply) {
stone(send)
if (!_cell.args.id) _cell.id = guid()
else _cell.id = _cell.args.id
$_.self = create_actor({id: _cell.id})
// Actor's timeslice for processing a single message
function turn(msg)
{
@@ -1520,12 +1587,6 @@ if (config.actor_memory)
if (config.stack_max)
js.max_stacksize(config.system.stack_max);
overling = _cell.args.overling_id ? create_actor({id: _cell.args.overling_id}) : null
$_.overling = overling
root = _cell.args.root_id ? create_actor({id: _cell.args.root_id}) : null
if (root == null) root = $_.self
if (overling) {
$_.couple(overling)
report_to_overling({type:'greet', actor_id: _cell.id})

View File

@@ -117,10 +117,10 @@ JSC_CCALL(fd_read,
JSC_SCALL(fd_slurp,
struct stat st;
if (stat(str, &st) != 0)
return JS_RaiseDisrupt(js, "stat failed: %s", strerror(errno));
return JS_RaiseDisrupt(js, "stat failed for %s: %s", str, strerror(errno));
if (!S_ISREG(st.st_mode))
return JS_RaiseDisrupt(js, "path is not a regular file");
return JS_RaiseDisrupt(js, "path %s is not a regular file", str);
size_t size = st.st_size;
if (size == 0)
@@ -636,7 +636,8 @@ static void visit_directory(JSContext *js, JSValue *results, int *result_count,
} else {
strcpy(item_rel, ffd.cFileName);
}
JS_SetPropertyNumber(js, *results, (*result_count)++, JS_NewString(js, item_rel));
JSValue name_str = JS_NewString(js, item_rel);
JS_SetPropertyNumber(js, *results, (*result_count)++, name_str);
if (recurse) {
struct stat st;
@@ -661,7 +662,8 @@ static void visit_directory(JSContext *js, JSValue *results, int *result_count,
} else {
strcpy(item_rel, dir->d_name);
}
JS_SetPropertyNumber(js, *results, (*result_count)++, JS_NewString(js, item_rel));
JSValue name_str = JS_NewString(js, item_rel);
JS_SetPropertyNumber(js, *results, (*result_count)++, name_str);
if (recurse) {
struct stat st;
@@ -761,6 +763,22 @@ JSC_CCALL(fd_readlink,
#endif
)
JSC_CCALL(fd_on_readable,
int fd = js2fd(js, argv[0]);
if (fd < 0) return JS_EXCEPTION;
if (!JS_IsFunction(argv[1]))
return JS_RaiseDisrupt(js, "on_readable: callback must be a function");
actor_watch_readable(js, fd, argv[1]);
return JS_NULL;
)
JSC_CCALL(fd_unwatch,
int fd = js2fd(js, argv[0]);
if (fd < 0) return JS_EXCEPTION;
actor_unwatch(js, fd);
return JS_NULL;
)
static const JSCFunctionListEntry js_fd_funcs[] = {
MIST_FUNC_DEF(fd, open, 2),
MIST_FUNC_DEF(fd, write, 2),
@@ -787,6 +805,8 @@ static const JSCFunctionListEntry js_fd_funcs[] = {
MIST_FUNC_DEF(fd, symlink, 2),
MIST_FUNC_DEF(fd, realpath, 1),
MIST_FUNC_DEF(fd, readlink, 1),
MIST_FUNC_DEF(fd, on_readable, 2),
MIST_FUNC_DEF(fd, unwatch, 1),
};
JSValue js_core_internal_fd_use(JSContext *js) {

View File

@@ -703,6 +703,27 @@ static JSValue js_os_stack(JSContext *js, JSValue self, int argc, JSValue *argv)
JS_RETURN(arr.val);
}
static JSValue js_os_unstone(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (argc < 1) return JS_NULL;
JSValue obj = argv[0];
if (mist_is_blob(obj)) {
JSBlob *bd = (JSBlob *)chase(obj);
bd->mist_hdr = objhdr_set_s(bd->mist_hdr, false);
return obj;
}
if (JS_IsArray(obj)) {
JSArray *arr = JS_VALUE_GET_ARRAY(obj);
arr->mist_hdr = objhdr_set_s(arr->mist_hdr, false);
return obj;
}
if (mist_is_gc_object(obj)) {
JSRecord *rec = JS_VALUE_GET_RECORD(obj);
rec->mist_hdr = objhdr_set_s(rec->mist_hdr, false);
return obj;
}
return JS_NULL;
}
static const JSCFunctionListEntry js_os_funcs[] = {
MIST_FUNC_DEF(os, platform, 0),
MIST_FUNC_DEF(os, arch, 0),
@@ -731,6 +752,7 @@ static const JSCFunctionListEntry js_os_funcs[] = {
MIST_FUNC_DEF(os, getenv, 1),
MIST_FUNC_DEF(os, qbe, 1),
MIST_FUNC_DEF(os, stack, 1),
MIST_FUNC_DEF(os, unstone, 1),
};
JSValue js_core_internal_os_use(JSContext *js) {

View File

@@ -455,6 +455,7 @@ Shop.extract_commit_hash = function(pkg, response) {
var open_dls = {}
var package_dylibs = {} // pkg -> [{file, symbol, dylib}, ...]
var reload_hashes = {} // cache_key -> content hash for reload change detection
function open_dylib_cached(path) {
var handle = open_dls[path]
@@ -1882,6 +1883,7 @@ Shop.sync_with_deps = function(pkg, opts) {
if (visited[current]) continue
visited[current] = true
log.console(' Fetching ' + current + '...')
Shop.sync(current, opts)
_read_deps = function() {
@@ -1981,19 +1983,27 @@ Shop.file_reload = function(file)
}
Shop.module_reload = function(path, package) {
if (!Shop.is_loaded(path,package)) return
if (!Shop.is_loaded(path, package)) return false
// Clear the module info cache for this path
var lookup_key = package ? package + ':' + path : ':' + path
module_info_cache[lookup_key] = null
var info = resolve_module_info(path, package)
if (!info) return false
// Invalidate package dylib cache so next resolve triggers rebuild
if (package) {
package_dylibs[package] = null
// Check if source actually changed
var mod_path = null
var source = null
var new_hash = null
if (info.mod_resolve) mod_path = info.mod_resolve.path
if (mod_path && fd.is_file(mod_path)) {
source = fd.slurp(mod_path)
new_hash = content_hash(stone(blob(text(source))))
if (reload_hashes[info.cache_key] == new_hash) return false
reload_hashes[info.cache_key] = new_hash
}
var info = resolve_module_info(path, package)
if (!info) return
// Clear caches
module_info_cache[lookup_key] = null
if (package) package_dylibs[package] = null
var cache_key = info.cache_key
var old = use_cache[cache_key]
@@ -2002,13 +2012,18 @@ Shop.module_reload = function(path, package) {
var newmod = get_module(path, package)
use_cache[cache_key] = newmod
// Smart update: unstone -> merge -> re-stone to preserve references
if (old && is_object(old) && is_object(newmod)) {
os.unstone(old)
arrfor(array(newmod), function(k) { old[k] = newmod[k] })
arrfor(array(old), function(k) {
if (!(k in newmod)) old[k] = null
})
stone(old)
use_cache[cache_key] = old
}
return true
}
function get_package_scripts(package)
@@ -2072,6 +2087,12 @@ Shop.build_package_scripts = function(package)
_try()
})
if (length(errors) > 0) {
log.console(' Compiling scripts (' + text(ok) + ' ok, ' + text(length(errors)) + ' errors)')
} else if (ok > 0) {
log.console(' Compiling scripts (' + text(ok) + ' ok)')
}
return {ok: ok, errors: errors, total: length(scripts)}
}

494
log.ce
View File

@@ -1,15 +1,17 @@
// cell log - Manage and read log sinks
// cell log - Manage log sink configuration
//
// Usage:
// cell log list List configured sinks
// cell log list Show sinks and channel routing
// cell log channels List channels with status
// cell log enable <channel> Enable a channel on terminal
// cell log disable <channel> Disable a channel on terminal
// cell log add <name> console [opts] Add a console sink
// cell log add <name> file <path> [opts] Add a file sink
// cell log remove <name> Remove a sink
// cell log read <sink> [opts] Read from a file sink
// cell log tail <sink> [--lines=N] Follow a file sink
//
// The --stack option controls which channels capture a stack trace.
// Default: --stack=error (errors always show a stack trace).
// cell log route <channel> <sink> Route a channel to a sink
// cell log unroute <channel> <sink> Remove a channel from a sink
// cell log stack <channel> Enable stack traces on a channel
// cell log unstack <channel> Disable stack traces on a channel
var toml = use('toml')
var fd = use('fd')
@@ -18,9 +20,8 @@ var json = use('json')
var log_path = shop_path + '/log.toml'
function load_config() {
if (fd.is_file(log_path)) {
if (fd.is_file(log_path))
return toml.decode(text(fd.slurp(log_path)))
}
return null
}
@@ -45,23 +46,24 @@ function print_help() {
log.console("Usage: cell log <command> [options]")
log.console("")
log.console("Commands:")
log.console(" list List configured sinks")
log.console(" list Show sinks and channel routing")
log.console(" channels List channels with status")
log.console(" enable <channel> Enable a channel on terminal")
log.console(" disable <channel> Disable a channel on terminal")
log.console(" add <name> console [opts] Add a console sink")
log.console(" add <name> file <path> [opts] Add a file sink")
log.console(" remove <name> Remove a sink")
log.console(" read <sink> [opts] Read from a file sink")
log.console(" tail <sink> [--lines=N] Follow a file sink")
log.console(" route <channel> <sink> Route a channel to a sink")
log.console(" unroute <channel> <sink> Remove a channel from a sink")
log.console(" stack <channel> Enable stack traces on a channel")
log.console(" unstack <channel> Disable stack traces on a channel")
log.console("")
log.console("Options for add:")
log.console(" --format=pretty|bare|json Output format (default: pretty for console, json for file)")
log.console(" --channels=ch1,ch2 Channels to subscribe (default: console,error,system)")
log.console(" --exclude=ch1,ch2 Channels to exclude (for wildcard sinks)")
log.console(" --stack=ch1,ch2 Channels that capture a stack trace (default: error)")
log.console("")
log.console("Options for read:")
log.console(" --lines=N Show last N lines (default: all)")
log.console(" --channel=X Filter by channel")
log.console(" --since=timestamp Only show entries after timestamp")
log.console(" --channels=ch1,ch2 Channels to subscribe (default: *)")
log.console(" --exclude=ch1,ch2 Channels to exclude")
log.console(" --mode=append|overwrite File write mode (default: append)")
log.console(" --max_size=N Max file size in bytes before truncation")
}
function parse_opt(arg, prefix) {
@@ -71,36 +73,85 @@ function parse_opt(arg, prefix) {
return null
}
function format_entry(entry) {
var aid = text(entry.actor_id, 0, 5)
var src = ""
var ev = null
if (entry.source && entry.source.file)
src = entry.source.file + ":" + text(entry.source.line)
ev = is_text(entry.event) ? entry.event : json.encode(entry.event)
return "[" + aid + "] [" + entry.channel + "] " + src + " " + ev
// Collect all stack channels across all sinks
function collect_stack_channels(config) {
var stack_chs = {}
var names = array(config.sink)
arrfor(names, function(n) {
var s = config.sink[n]
if (is_array(s.stack)) {
arrfor(s.stack, function(ch) { stack_chs[ch] = true })
}
})
return stack_chs
}
// Find which sinks a stack channel is declared on (for modification)
function find_stack_sink(config, channel) {
var names = array(config.sink)
var found = null
arrfor(names, function(n) {
if (found) return
var s = config.sink[n]
if (is_array(s.stack)) {
arrfor(s.stack, function(ch) {
if (ch == channel) found = n
})
}
})
return found
}
function do_list() {
var config = load_config()
var names = null
var channel_routing = {}
var stack_chs = null
names = (config && config.sink) ? array(config.sink) : []
if (length(names) == 0) {
log.console("No log sinks configured.")
log.console("Default: console pretty for console/error/system (stack traces on error)")
return
}
// Show sinks
log.console("Sinks:")
arrfor(names, function(n) {
var s = config.sink[n]
var ch = is_array(s.channels) ? text(s.channels, ', ') : '(none)'
var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : ""
var stk = is_array(s.stack) ? " stack=" + text(s.stack, ',') : ""
var fmt = s.format || (s.type == 'file' ? 'json' : 'pretty')
var mode = s.mode ? " mode=" + s.mode : ""
var maxsz = s.max_size ? " max_size=" + text(s.max_size) : ""
var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : ""
if (s.type == 'file')
log.console(" " + n + ": " + s.type + " -> " + s.path + " [" + ch + "] format=" + fmt + ex + stk)
log.console(" " + n + ": file -> " + s.path + " format=" + fmt + mode + maxsz)
else
log.console(" " + n + ": " + s.type + " [" + ch + "] format=" + fmt + ex + stk)
log.console(" " + n + ": console format=" + fmt + ex)
})
// Build channel -> sinks map
arrfor(names, function(n) {
var s = config.sink[n]
var chs = is_array(s.channels) ? s.channels : []
arrfor(chs, function(ch) {
if (!channel_routing[ch]) channel_routing[ch] = []
channel_routing[ch][] = n
})
})
// Show routing
log.console("")
log.console("Routing:")
var channels = array(channel_routing)
arrfor(channels, function(ch) {
log.console(" " + ch + " -> " + text(channel_routing[ch], ', '))
})
// Show stack traces
stack_chs = collect_stack_channels(config)
var stack_list = array(stack_chs)
if (length(stack_list) > 0) {
log.console("")
log.console("Stack traces on: " + text(stack_list, ', '))
}
}
function do_add() {
@@ -108,14 +159,15 @@ function do_add() {
var sink_type = null
var path = null
var format = null
var channels = ["console", "error", "system"]
var channels = ["*"]
var exclude = null
var stack_chs = ["error"]
var mode = null
var max_size = null
var config = null
var val = null
var i = 0
if (length(args) < 3) {
log.error("Usage: cell log add <name> console|file [path] [options]")
log.console("Usage: cell log add <name> console|file [path] [options]")
return
}
name = args[1]
@@ -123,7 +175,7 @@ function do_add() {
if (sink_type == 'file') {
if (length(args) < 4) {
log.error("Usage: cell log add <name> file <path> [options]")
log.console("Usage: cell log add <name> file <path> [options]")
return
}
path = args[3]
@@ -133,7 +185,7 @@ function do_add() {
format = "pretty"
i = 3
} else {
log.error("Unknown sink type: " + sink_type + " (use 'console' or 'file')")
log.console("Unknown sink type: " + sink_type + " (use 'console' or 'file')")
return
}
@@ -144,17 +196,21 @@ function do_add() {
if (val) { channels = array(val, ','); continue }
val = parse_opt(args[i], 'exclude')
if (val) { exclude = array(val, ','); continue }
val = parse_opt(args[i], 'stack')
if (val) { stack_chs = array(val, ','); continue }
val = parse_opt(args[i], 'mode')
if (val) { mode = val; continue }
val = parse_opt(args[i], 'max_size')
if (val) { max_size = number(val); continue }
}
config = load_config()
if (!config) config = {}
if (!config.sink) config.sink = {}
config.sink[name] = {type: sink_type, format: format, channels: channels, stack: stack_chs}
config.sink[name] = {type: sink_type, format: format, channels: channels}
if (path) config.sink[name].path = path
if (exclude) config.sink[name].exclude = exclude
if (mode) config.sink[name].mode = mode
if (max_size) config.sink[name].max_size = max_size
save_config(config)
log.console("Added sink: " + name)
@@ -164,13 +220,13 @@ function do_remove() {
var name = null
var config = null
if (length(args) < 2) {
log.error("Usage: cell log remove <name>")
log.console("Usage: cell log remove <name>")
return
}
name = args[1]
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.error("Sink not found: " + name)
log.console("Sink not found: " + name)
return
}
delete config.sink[name]
@@ -178,154 +234,242 @@ function do_remove() {
log.console("Removed sink: " + name)
}
function do_read() {
var name = null
var max_lines = 0
var filter_channel = null
var since = 0
function do_route() {
var channel = null
var sink_name = null
var config = null
var sink = null
var content = null
var lines = null
var entries = []
var entry = null
var val = null
var i = 0
if (length(args) < 2) {
log.error("Usage: cell log read <sink_name> [options]")
var already = false
if (length(args) < 3) {
log.console("Usage: cell log route <channel> <sink>")
return
}
name = args[1]
for (i = 2; i < length(args); i++) {
val = parse_opt(args[i], 'lines')
if (val) { max_lines = number(val); continue }
val = parse_opt(args[i], 'channel')
if (val) { filter_channel = val; continue }
val = parse_opt(args[i], 'since')
if (val) { since = number(val); continue }
}
channel = args[1]
sink_name = args[2]
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.error("Sink not found: " + name)
if (!config || !config.sink || !config.sink[sink_name]) {
log.console("Sink not found: " + sink_name)
return
}
sink = config.sink[name]
if (sink.type != 'file') {
log.error("Can only read from file sinks")
return
}
if (!fd.is_file(sink.path)) {
log.console("Log file does not exist yet: " + sink.path)
return
}
content = text(fd.slurp(sink.path))
lines = array(content, '\n')
arrfor(lines, function(line) {
var parse_fn = null
if (length(line) == 0) return
parse_fn = function() {
entry = json.decode(line)
} disruption {
entry = null
}
parse_fn()
if (!entry) return
if (filter_channel && entry.channel != filter_channel) return
if (since > 0 && entry.timestamp < since) return
entries[] = entry
})
if (max_lines > 0 && length(entries) > max_lines)
entries = array(entries, length(entries) - max_lines, length(entries))
arrfor(entries, function(e) {
log.console(format_entry(e))
sink = config.sink[sink_name]
if (!is_array(sink.channels)) sink.channels = []
arrfor(sink.channels, function(ch) {
if (ch == channel) already = true
})
if (already) {
log.console(channel + " already routed to " + sink_name)
return
}
sink.channels[] = channel
save_config(config)
log.console(channel + " -> " + sink_name)
}
function do_tail() {
var name = null
var tail_lines = 10
function do_unroute() {
var channel = null
var sink_name = null
var config = null
var sink = null
var last_size = 0
var val = null
var i = 0
if (length(args) < 2) {
log.error("Usage: cell log tail <sink_name> [--lines=N]")
var found = false
if (length(args) < 3) {
log.console("Usage: cell log unroute <channel> <sink>")
return
}
name = args[1]
for (i = 2; i < length(args); i++) {
val = parse_opt(args[i], 'lines')
if (val) { tail_lines = number(val); continue }
}
channel = args[1]
sink_name = args[2]
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.error("Sink not found: " + name)
if (!config || !config.sink || !config.sink[sink_name]) {
log.console("Sink not found: " + sink_name)
return
}
sink = config.sink[name]
if (sink.type != 'file') {
log.error("Can only tail file sinks")
sink = config.sink[sink_name]
if (!is_array(sink.channels)) sink.channels = []
sink.channels = filter(sink.channels, function(ch) { return ch != channel })
save_config(config)
log.console(channel + " removed from " + sink_name)
}
function do_stack() {
var channel = null
var config = null
var names = null
var added = false
if (length(args) < 2) {
log.console("Usage: cell log stack <channel>")
return
}
if (!fd.is_file(sink.path))
log.console("Waiting for log file: " + sink.path)
function poll() {
var st = null
var poll_content = null
var poll_lines = null
var start = 0
var poll_entry = null
var old_line_count = 0
var idx = 0
var parse_fn = null
if (!fd.is_file(sink.path)) {
$delay(poll, 1)
return
channel = args[1]
config = load_config()
if (!config || !config.sink) {
log.console("No sinks configured")
return
}
// Add to first sink that already has a stack array, or first sink overall
names = array(config.sink)
arrfor(names, function(n) {
var s = config.sink[n]
var already = false
if (added) return
if (is_array(s.stack)) {
arrfor(s.stack, function(ch) { if (ch == channel) already = true })
if (!already) s.stack[] = channel
added = true
}
st = fd.stat(sink.path)
if (st.size == last_size) {
$delay(poll, 1)
return
})
if (!added && length(names) > 0) {
config.sink[names[0]].stack = [channel]
added = true
}
if (added) {
save_config(config)
log.console("Stack traces enabled on: " + channel)
}
}
function do_unstack() {
var channel = null
var config = null
var names = null
if (length(args) < 2) {
log.console("Usage: cell log unstack <channel>")
return
}
channel = args[1]
config = load_config()
if (!config || !config.sink) {
log.console("No sinks configured")
return
}
names = array(config.sink)
arrfor(names, function(n) {
var s = config.sink[n]
if (is_array(s.stack))
s.stack = filter(s.stack, function(ch) { return ch != channel })
})
save_config(config)
log.console("Stack traces disabled on: " + channel)
}
var known_channels = ["console", "error", "warn", "system", "build", "shop", "compile", "test"]
function find_terminal_sink(config) {
var names = null
var found = null
if (!config || !config.sink) return null
names = array(config.sink)
if (config.sink.terminal) return config.sink.terminal
arrfor(names, function(n) {
if (!found && config.sink[n].type == "console")
found = config.sink[n]
})
return found
}
function do_enable() {
var channel = null
var config = null
var sink = null
var i = 0
var already = false
if (length(args) < 2) {
log.error("Usage: cell log enable <channel>")
return
}
channel = args[1]
config = load_config()
if (!config) config = {sink: {}}
if (!config.sink) config.sink = {}
sink = find_terminal_sink(config)
if (!sink) {
config.sink.terminal = {type: "console", format: "clean", channels: ["console", "error", channel], stack: ["error"]}
save_config(config)
log.console("Enabled channel: " + channel)
return
}
if (is_array(sink.channels) && length(sink.channels) == 1 && sink.channels[0] == "*") {
if (is_array(sink.exclude)) {
var new_exclude = []
arrfor(sink.exclude, function(ex) {
if (ex != channel) push(new_exclude, ex)
})
sink.exclude = new_exclude
}
} else {
if (!is_array(sink.channels)) sink.channels = ["console", "error"]
arrfor(sink.channels, function(ch) {
if (ch == channel) already = true
})
if (!already) sink.channels[] = channel
}
save_config(config)
log.console("Enabled channel: " + channel)
}
poll_content = text(fd.slurp(sink.path))
poll_lines = array(poll_content, '\n')
if (last_size == 0 && length(poll_lines) > tail_lines) {
start = length(poll_lines) - tail_lines
} else if (last_size > 0) {
old_line_count = length(array(text(poll_content, 0, last_size), '\n'))
start = old_line_count
function do_disable() {
var channel = null
var config = null
var sink = null
var i = 0
var new_channels = []
if (length(args) < 2) {
log.error("Usage: cell log disable <channel>")
return
}
channel = args[1]
config = load_config()
if (!config || !config.sink) {
log.error("No log configuration found")
return
}
sink = find_terminal_sink(config)
if (!sink) {
log.error("No terminal sink found")
return
}
if (is_array(sink.channels) && length(sink.channels) == 1 && sink.channels[0] == "*") {
if (!is_array(sink.exclude)) sink.exclude = []
var already_excluded = false
arrfor(sink.exclude, function(ex) {
if (ex == channel) already_excluded = true
})
if (!already_excluded) sink.exclude[] = channel
} else {
if (is_array(sink.channels)) {
arrfor(sink.channels, function(ch) {
if (ch != channel) push(new_channels, ch)
})
sink.channels = new_channels
}
}
save_config(config)
log.console("Disabled channel: " + channel)
}
last_size = st.size
for (idx = start; idx < length(poll_lines); idx++) {
if (length(poll_lines[idx]) == 0) continue
parse_fn = function() {
poll_entry = json.decode(poll_lines[idx])
} disruption {
poll_entry = null
function do_channels() {
var config = load_config()
var sink = null
var is_wildcard = false
var active = {}
if (config) sink = find_terminal_sink(config)
if (sink) {
if (is_array(sink.channels) && length(sink.channels) == 1 && sink.channels[0] == "*") {
is_wildcard = true
arrfor(known_channels, function(ch) { active[ch] = true })
if (is_array(sink.exclude)) {
arrfor(sink.exclude, function(ex) { active[ex] = false })
}
parse_fn()
if (!poll_entry) continue
os.print(format_entry(poll_entry) + "\n")
} else if (is_array(sink.channels)) {
arrfor(sink.channels, function(ch) { active[ch] = true })
}
$delay(poll, 1)
} else {
active.console = true
active.error = true
}
poll()
log.console("Channels:")
arrfor(known_channels, function(ch) {
var status = active[ch] ? "enabled" : "disabled"
log.console(" " + ch + ": " + status)
})
}
// Main dispatch
@@ -335,16 +479,26 @@ if (length(args) == 0) {
print_help()
} else if (args[0] == 'list') {
do_list()
} else if (args[0] == 'channels') {
do_channels()
} else if (args[0] == 'enable') {
do_enable()
} else if (args[0] == 'disable') {
do_disable()
} else if (args[0] == 'add') {
do_add()
} else if (args[0] == 'remove') {
do_remove()
} else if (args[0] == 'read') {
do_read()
} else if (args[0] == 'tail') {
do_tail()
} else if (args[0] == 'route') {
do_route()
} else if (args[0] == 'unroute') {
do_unroute()
} else if (args[0] == 'stack') {
do_stack()
} else if (args[0] == 'unstack') {
do_unstack()
} else {
log.error("Unknown command: " + args[0])
log.console("Unknown command: " + args[0])
print_help()
}

View File

@@ -34,6 +34,7 @@ if host_machine.system() == 'darwin'
fworks = [
'CoreFoundation',
'CFNetwork',
'Security',
]
foreach fkit : fworks
deps += dependency('appleframeworks', modules: fkit)
@@ -82,6 +83,7 @@ scripts = [
'internal/os.c',
'internal/fd.c',
'net/http.c',
'net/tls.c',
'net/socket.c',
'internal/enet.c',
'archive/miniz.c',

View File

@@ -24,6 +24,9 @@
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#ifndef _WIN32
#include <fcntl.h>
#endif
// Helper to convert JS value to file descriptor
static int js2fd(JSContext *ctx, JSValueConst val)
@@ -582,6 +585,87 @@ JSC_CCALL(socket_unwatch,
return JS_NULL;
)
JSC_CCALL(socket_on_writable,
int sockfd = js2fd(js, argv[0]);
if (sockfd < 0) return JS_EXCEPTION;
if (!JS_IsFunction(argv[1]))
return JS_RaiseDisrupt(js, "on_writable: callback must be a function");
actor_watch_writable(js, sockfd, argv[1]);
return JS_NULL;
)
JSC_CCALL(socket_setnonblock,
int sockfd = js2fd(js, argv[0]);
if (sockfd < 0) return JS_EXCEPTION;
#ifdef _WIN32
u_long mode = 1;
if (ioctlsocket(sockfd, FIONBIO, &mode) != 0)
return JS_RaiseDisrupt(js, "setnonblock failed");
#else
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags < 0 || fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0)
return JS_RaiseDisrupt(js, "setnonblock failed: %s", strerror(errno));
#endif
return JS_NULL;
)
JSC_CCALL(socket_getsockopt,
int sockfd = js2fd(js, argv[0]);
if (sockfd < 0) return JS_EXCEPTION;
int level = SOL_SOCKET;
int optname = 0;
// Parse level
if (JS_IsText(argv[1])) {
const char *level_str = JS_ToCString(js, argv[1]);
if (strcmp(level_str, "SOL_SOCKET") == 0) level = SOL_SOCKET;
else if (strcmp(level_str, "IPPROTO_TCP") == 0) level = IPPROTO_TCP;
else if (strcmp(level_str, "IPPROTO_IP") == 0) level = IPPROTO_IP;
else if (strcmp(level_str, "IPPROTO_IPV6") == 0) level = IPPROTO_IPV6;
JS_FreeCString(js, level_str);
} else {
level = js2number(js, argv[1]);
}
// Parse option name
if (JS_IsText(argv[2])) {
const char *opt_str = JS_ToCString(js, argv[2]);
if (strcmp(opt_str, "SO_ERROR") == 0) optname = SO_ERROR;
else if (strcmp(opt_str, "SO_REUSEADDR") == 0) optname = SO_REUSEADDR;
else if (strcmp(opt_str, "SO_KEEPALIVE") == 0) optname = SO_KEEPALIVE;
else if (strcmp(opt_str, "SO_BROADCAST") == 0) optname = SO_BROADCAST;
JS_FreeCString(js, opt_str);
} else {
optname = js2number(js, argv[2]);
}
int optval = 0;
socklen_t optlen = sizeof(optval);
if (getsockopt(sockfd, level, optname, &optval, &optlen) < 0)
return JS_RaiseDisrupt(js, "getsockopt failed: %s", strerror(errno));
return JS_NewInt32(js, optval);
)
JSC_CCALL(socket_send_self,
if (argc < 1 || !JS_IsText(argv[0]))
return JS_RaiseDisrupt(js, "send_self: expects a text argument");
const char *msg = JS_ToCString(js, argv[0]);
WotaBuffer wb;
wota_buffer_init(&wb, 16);
wota_write_record(&wb, 1);
wota_write_text(&wb, "text");
wota_write_text(&wb, msg);
JS_FreeCString(js, msg);
const char *err = JS_SendMessage(js, &wb);
if (err) {
wota_buffer_free(&wb);
return JS_RaiseDisrupt(js, "send_self failed: %s", err);
}
return JS_NULL;
)
static const JSCFunctionListEntry js_socket_funcs[] = {
MIST_FUNC_DEF(socket, getaddrinfo, 3),
MIST_FUNC_DEF(socket, socket, 3),
@@ -600,7 +684,11 @@ static const JSCFunctionListEntry js_socket_funcs[] = {
MIST_FUNC_DEF(socket, setsockopt, 4),
MIST_FUNC_DEF(socket, close, 1),
MIST_FUNC_DEF(socket, on_readable, 2),
MIST_FUNC_DEF(socket, on_writable, 2),
MIST_FUNC_DEF(socket, unwatch, 1),
MIST_FUNC_DEF(socket, setnonblock, 1),
MIST_FUNC_DEF(socket, getsockopt, 3),
MIST_FUNC_DEF(socket, send_self, 1),
};
JSValue js_core_socket_use(JSContext *js) {
@@ -625,6 +713,8 @@ JSValue js_core_socket_use(JSContext *js) {
JS_SetPropertyStr(js, mod.val, "SOL_SOCKET", JS_NewInt32(js, SOL_SOCKET));
JS_SetPropertyStr(js, mod.val, "SO_REUSEADDR", JS_NewInt32(js, SO_REUSEADDR));
JS_SetPropertyStr(js, mod.val, "SO_ERROR", JS_NewInt32(js, SO_ERROR));
JS_SetPropertyStr(js, mod.val, "SO_KEEPALIVE", JS_NewInt32(js, SO_KEEPALIVE));
JS_RETURN(mod.val);
}

238
net/tls.c Normal file
View File

@@ -0,0 +1,238 @@
#include "cell.h"
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#if defined(__APPLE__)
/* SecureTransport — deprecated but functional, no external deps */
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#include <Security/Security.h>
#include <Security/SecureTransport.h>
#include <sys/socket.h>
#include <unistd.h>
#include <poll.h>
typedef struct {
SSLContextRef ssl;
int fd;
} tls_ctx;
static void tls_ctx_free(JSRuntime *rt, tls_ctx *ctx) {
if (!ctx) return;
if (ctx->ssl) {
SSLClose(ctx->ssl);
CFRelease(ctx->ssl);
}
if (ctx->fd >= 0)
close(ctx->fd);
free(ctx);
}
QJSCLASS(tls_ctx,)
static OSStatus tls_read_cb(SSLConnectionRef conn, void *data, size_t *len) {
int fd = *(const int *)conn;
size_t requested = *len;
size_t total = 0;
while (total < requested) {
ssize_t n = read(fd, (char *)data + total, requested - total);
if (n > 0) {
total += n;
} else if (n == 0) {
*len = total;
return errSSLClosedGraceful;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
*len = total;
return (total > 0) ? noErr : errSSLWouldBlock;
}
*len = total;
return errSSLClosedAbort;
}
}
*len = total;
return noErr;
}
static OSStatus tls_write_cb(SSLConnectionRef conn, const void *data, size_t *len) {
int fd = *(const int *)conn;
size_t requested = *len;
size_t total = 0;
while (total < requested) {
ssize_t n = write(fd, (const char *)data + total, requested - total);
if (n > 0) {
total += n;
} else if (n == 0) {
*len = total;
return errSSLClosedGraceful;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
*len = total;
return (total > 0) ? noErr : errSSLWouldBlock;
}
*len = total;
return errSSLClosedAbort;
}
}
*len = total;
return noErr;
}
/* tls.wrap(fd, hostname) -> ctx */
JSC_CCALL(tls_wrap,
int fd = -1;
if (JS_ToInt32(js, &fd, argv[0]) < 0)
return JS_RaiseDisrupt(js, "tls.wrap: fd must be a number");
const char *hostname = JS_ToCString(js, argv[1]);
if (!hostname)
return JS_RaiseDisrupt(js, "tls.wrap: hostname must be a string");
tls_ctx *ctx = calloc(1, sizeof(tls_ctx));
ctx->fd = fd;
ctx->ssl = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType);
if (!ctx->ssl) {
free(ctx);
JS_FreeCString(js, hostname);
return JS_RaiseDisrupt(js, "tls.wrap: SSLCreateContext failed");
}
SSLSetIOFuncs(ctx->ssl, tls_read_cb, tls_write_cb);
SSLSetConnection(ctx->ssl, &ctx->fd);
SSLSetPeerDomainName(ctx->ssl, hostname, strlen(hostname));
JS_FreeCString(js, hostname);
/* Retry handshake on non-blocking sockets (errSSLWouldBlock) */
OSStatus status;
for (int attempts = 0; attempts < 200; attempts++) {
status = SSLHandshake(ctx->ssl);
if (status == noErr) break;
if (status != errSSLWouldBlock) break;
struct pollfd pfd = { .fd = ctx->fd, .events = POLLIN | POLLOUT };
poll(&pfd, 1, 50);
}
if (status != noErr) {
CFRelease(ctx->ssl);
ctx->ssl = NULL;
ctx->fd = -1; /* don't close caller's fd */
free(ctx);
return JS_RaiseDisrupt(js, "tls.wrap: handshake failed (status %d)", (int)status);
}
return tls_ctx2js(js, ctx);
)
/* tls.send(ctx, data) -> bytes_sent */
JSC_CCALL(tls_send,
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
if (!ctx || !ctx->ssl)
return JS_RaiseDisrupt(js, "tls.send: invalid context");
size_t len;
size_t written = 0;
OSStatus status;
if (JS_IsText(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]);
status = SSLWrite(ctx->ssl, data, len, &written);
JS_FreeCString(js, data);
} else {
unsigned char *data = js_get_blob_data(js, &len, argv[1]);
if (!data)
return JS_RaiseDisrupt(js, "tls.send: invalid data");
status = SSLWrite(ctx->ssl, data, len, &written);
}
if (status != noErr && status != errSSLWouldBlock)
return JS_RaiseDisrupt(js, "tls.send: write failed (status %d)", (int)status);
return JS_NewInt64(js, (int64_t)written);
)
/* tls.recv(ctx, len) -> blob */
JSC_CCALL(tls_recv,
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
if (!ctx || !ctx->ssl)
return JS_RaiseDisrupt(js, "tls.recv: invalid context");
size_t len = 4096;
if (argc > 1) len = js2number(js, argv[1]);
void *out;
ret = js_new_blob_alloc(js, len, &out);
if (JS_IsException(ret)) return ret;
size_t received = 0;
OSStatus status = SSLRead(ctx->ssl, out, len, &received);
if (status != noErr && status != errSSLWouldBlock &&
status != errSSLClosedGraceful) {
return JS_RaiseDisrupt(js, "tls.recv: read failed (status %d)", (int)status);
}
js_blob_stone(ret, received);
return ret;
)
/* tls.close(ctx) -> null */
JSC_CCALL(tls_close,
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
if (!ctx) return JS_NULL;
if (ctx->ssl) {
SSLClose(ctx->ssl);
CFRelease(ctx->ssl);
ctx->ssl = NULL;
}
if (ctx->fd >= 0) {
close(ctx->fd);
ctx->fd = -1;
}
return JS_NULL;
)
/* tls.fd(ctx) -> number — get underlying fd for on_readable */
JSC_CCALL(tls_fd,
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
if (!ctx)
return JS_RaiseDisrupt(js, "tls.fd: invalid context");
return JS_NewInt32(js, ctx->fd);
)
/* tls.on_readable(ctx, callback) -> null */
JSC_CCALL(tls_on_readable,
tls_ctx *ctx = js2tls_ctx(js, argv[0]);
if (!ctx)
return JS_RaiseDisrupt(js, "tls.on_readable: invalid context");
if (!JS_IsFunction(argv[1]))
return JS_RaiseDisrupt(js, "tls.on_readable: callback must be a function");
actor_watch_readable(js, ctx->fd, argv[1]);
return JS_NULL;
)
static const JSCFunctionListEntry js_tls_funcs[] = {
MIST_FUNC_DEF(tls, wrap, 2),
MIST_FUNC_DEF(tls, send, 2),
MIST_FUNC_DEF(tls, recv, 2),
MIST_FUNC_DEF(tls, close, 1),
MIST_FUNC_DEF(tls, fd, 1),
MIST_FUNC_DEF(tls, on_readable, 2),
};
JSValue js_core_net_tls_use(JSContext *js) {
JS_FRAME(js);
QJSCLASSPREP_NO_FUNCS(tls_ctx);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_tls_funcs, countof(js_tls_funcs));
JS_RETURN(mod.val);
}
#pragma clang diagnostic pop
#else
/* Stub for non-Apple platforms — TLS not yet implemented */
JSValue js_core_net_tls_use(JSContext *js) {
return JS_RaiseDisrupt(js, "TLS not available on this platform");
}
#endif

View File

@@ -4,6 +4,7 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "wota.h"
#ifdef __cplusplus
extern "C" {
@@ -1150,6 +1151,18 @@ JSValue CELL_USE_NAME(JSContext *js) { \
#define CELL_PROGRAM_INIT(c) \
JSValue CELL_USE_NAME(JSContext *js) { do { c ; } while(0); }
/* ============================================================
WOTA Message Sending — C modules can send messages to actors.
============================================================ */
/* Check whether an actor with the given ID exists. */
int JS_ActorExists(const char *actor_id);
/* Send a WOTA-encoded message to the actor that owns ctx.
Takes ownership of wb's data on success (caller must not free).
On failure returns an error string; caller must free wb. */
const char *JS_SendMessage(JSContext *ctx, WotaBuffer *wb);
#undef js_unlikely
#undef inline

View File

@@ -133,7 +133,8 @@ static void js_log_callback(JSContext *ctx, const char *channel, const char *msg
JS_FRAME(ctx);
JS_ROOT(stack, JS_GetStack(ctx));
JS_ROOT(args_array, JS_NewArray(ctx));
JS_SetPropertyNumber(ctx, args_array.val, 0, JS_NewString(ctx, msg));
JSValue msg_str = JS_NewString(ctx, msg);
JS_SetPropertyNumber(ctx, args_array.val, 0, msg_str);
JS_SetPropertyNumber(ctx, args_array.val, 1, stack.val);
JSValue argv[2];
argv[0] = JS_NewString(ctx, channel);

View File

@@ -3277,7 +3277,8 @@ JS_RaiseDisrupt (JSContext *ctx, const char *fmt, ...) {
va_start (ap, fmt);
vsnprintf (buf, sizeof (buf), fmt, ap);
va_end (ap);
JS_Log (ctx, "error", "%s", buf);
if (ctx->log_callback)
JS_Log (ctx, "error", "%s", buf);
ctx->current_exception = JS_TRUE;
return JS_EXCEPTION;
}

View File

@@ -1054,3 +1054,57 @@ JSValue actor_remove_timer(JSContext *actor, uint32_t timer_id)
// Note: We don't remove from heap, it will misfire safely
return cb;
}
int JS_ActorExists(const char *actor_id)
{
return actor_exists(actor_id);
}
const char *JS_SendMessage(JSContext *ctx, WotaBuffer *wb)
{
if (!wb || !wb->data || wb->size == 0)
return "Empty WOTA buffer";
/* Wrap the caller's payload in the engine protocol envelope:
{type: "user", data: <payload>}
The header takes ~6 words; pre-allocate enough for header + payload. */
WotaBuffer envelope;
wota_buffer_init(&envelope, wb->size + 8);
wota_write_record(&envelope, 2);
wota_write_text(&envelope, "type");
wota_write_text(&envelope, "user");
wota_write_text(&envelope, "data");
/* Append the caller's pre-encoded WOTA payload words directly. */
size_t need = envelope.size + wb->size;
if (need > envelope.capacity) {
size_t new_cap = envelope.capacity ? envelope.capacity * 2 : 8;
while (new_cap < need) new_cap *= 2;
envelope.data = realloc(envelope.data, new_cap * sizeof(uint64_t));
envelope.capacity = new_cap;
}
memcpy(envelope.data + envelope.size, wb->data,
wb->size * sizeof(uint64_t));
envelope.size += wb->size;
size_t byte_len = envelope.size * sizeof(uint64_t);
blob *msg = blob_new(byte_len * 8);
if (!msg) {
wota_buffer_free(&envelope);
return "Could not allocate blob";
}
blob_write_bytes(msg, envelope.data, byte_len);
blob_make_stone(msg);
wota_buffer_free(&envelope);
const char *err = send_message(ctx->id, msg);
if (!err) {
/* Success — send_message took ownership of the blob.
Free the WotaBuffer internals since we consumed them. */
wota_buffer_free(wb);
}
/* On failure, send_message already destroyed the blob.
Caller still owns wb and must free it. */
return err;
}

View File

@@ -278,7 +278,11 @@ var streamline = function(ir, log) {
store_index: [1, T_ARRAY, 2, T_INT], store_field: [1, T_RECORD],
push: [1, T_ARRAY],
load_index: [2, T_ARRAY, 3, T_INT], load_field: [2, T_RECORD],
pop: [2, T_ARRAY]
pop: [2, T_ARRAY],
is_text: [2, T_UNKNOWN], is_int: [2, T_UNKNOWN], is_num: [2, T_UNKNOWN],
is_bool: [2, T_UNKNOWN], is_null: [2, T_UNKNOWN],
is_array: [2, T_UNKNOWN], is_func: [2, T_UNKNOWN],
is_record: [2, T_UNKNOWN], is_blob: [2, T_UNKNOWN]
}
var infer_param_types = function(func) {

251
tests/cellfs_test.ce Normal file
View File

@@ -0,0 +1,251 @@
// Test: cellfs mounting, sync access, and async requestors
//
// Known limitation:
// - is_directory() uses raw path instead of res.path for fs mounts,
// so @-prefixed paths fail (e.g. '@mount/dir'). Works via stat().
// - cellfs.slurp() has no http code path; use cellfs.get() for http mounts.
var cellfs = use('cellfs')
var fd = use('fd')
var pkg_dir = '.cell/packages/gitea.pockle.world/john/prosperon'
// Mount the prosperon package directory as 'prosperon'
cellfs.mount(pkg_dir, 'prosperon')
// --- exists ---
var found = cellfs.exists('@prosperon/color.cm')
if (!found) {
log.error("exists('@prosperon/color.cm') returned false")
disrupt
}
log.console("exists: ok")
// exists returns false for missing files
var missing = cellfs.exists('@prosperon/no_such_file.cm')
if (missing) {
log.error("exists returned true for missing file")
disrupt
}
log.console("exists (missing): ok")
// --- slurp ---
var data = cellfs.slurp('@prosperon/color.cm')
if (!is_blob(data)) {
log.error("slurp did not return a blob")
disrupt
}
if (length(data) == 0) {
log.error("slurp returned empty blob")
disrupt
}
log.console(`slurp: ok (${length(data)} bits)`)
// --- enumerate ---
var files = cellfs.enumerate('@prosperon', false)
if (!is_array(files)) {
log.error("enumerate did not return an array")
disrupt
}
if (length(files) == 0) {
log.error("enumerate returned empty array")
disrupt
}
// color.cm should be in the listing
var found_color = false
arrfor(files, function(f) {
if (f == 'color.cm') {
found_color = true
return true
}
}, false, true)
if (!found_color) {
log.error("enumerate did not include color.cm")
disrupt
}
log.console(`enumerate: ok (${length(files)} entries, found color.cm)`)
// enumerate recursive
var rfiles = cellfs.enumerate('@prosperon', true)
if (length(rfiles) <= length(files)) {
log.error("recursive enumerate should return more entries")
disrupt
}
log.console(`enumerate recursive: ok (${length(rfiles)} entries)`)
// --- stat ---
var st = cellfs.stat('@prosperon/color.cm')
if (!is_object(st)) {
log.error("stat did not return an object")
disrupt
}
if (st.filesize == null || st.filesize == 0) {
log.error("stat filesize missing or zero")
disrupt
}
log.console(`stat: ok (size=${st.filesize}, mtime=${st.modtime})`)
// stat on a directory
var dir_st = cellfs.stat('@prosperon/docs')
if (!dir_st.isDirectory) {
log.error("stat('@prosperon/docs').isDirectory returned false")
disrupt
}
log.console("stat (directory): ok")
// --- searchpath ---
var sp = cellfs.searchpath()
if (!is_array(sp)) {
log.error("searchpath did not return an array")
disrupt
}
if (length(sp) == 0) {
log.error("searchpath returned empty array")
disrupt
}
log.console(`searchpath: ok (${length(sp)} mounts)`)
// --- resolve ---
var res = cellfs.resolve('@prosperon/color.cm')
if (!is_object(res)) {
log.error("resolve did not return an object")
disrupt
}
if (res.mount.name != 'prosperon') {
log.error("resolve returned wrong mount name")
disrupt
}
if (res.path != 'color.cm') {
log.error("resolve returned wrong path")
disrupt
}
log.console("resolve: ok")
// --- realdir ---
var rd = cellfs.realdir('@prosperon/color.cm')
if (!is_text(rd)) {
log.error("realdir did not return text")
disrupt
}
if (!ends_with(rd, 'color.cm')) {
log.error("realdir does not end with color.cm")
disrupt
}
log.console(`realdir: ok (${rd})`)
// --- unmount and re-mount ---
cellfs.unmount('prosperon')
var after_unmount = cellfs.searchpath()
var unmount_ok = true
arrfor(after_unmount, function(m) {
if (m.name == 'prosperon') {
unmount_ok = false
return true
}
}, false, true)
if (!unmount_ok) {
log.error("unmount failed, mount still present")
disrupt
}
log.console("unmount: ok")
// re-mount for further tests
cellfs.mount(pkg_dir, 'prosperon')
// --- match (wildstar) ---
var m1 = cellfs.match('color.cm', '*.cm')
if (!m1) {
log.error("match('color.cm', '*.cm') returned false")
disrupt
}
var m2 = cellfs.match('color.cm', '*.ce')
if (m2) {
log.error("match('color.cm', '*.ce') returned true")
disrupt
}
log.console("match: ok")
// --- globfs ---
var cm_files = cellfs.globfs(['*.cm'], '@prosperon')
if (!is_array(cm_files)) {
log.error("globfs did not return an array")
disrupt
}
if (length(cm_files) == 0) {
log.error("globfs returned empty array")
disrupt
}
// all results should end in .cm
var all_cm = true
arrfor(cm_files, function(f) {
if (!ends_with(f, '.cm')) {
all_cm = false
return true
}
}, false, true)
if (!all_cm) {
log.error("globfs returned non-.cm files")
disrupt
}
log.console(`globfs: ok (${length(cm_files)} .cm files)`)
log.console("--- sync tests passed ---")
// --- Requestor tests ---
// get requestor for a local fs mount
var get_color = cellfs.get('@prosperon/color.cm')
get_color(function(result, reason) {
if (reason != null) {
log.error(`get color.cm failed: ${reason}`)
disrupt
}
if (!is_blob(result)) {
log.error("get did not return a blob")
disrupt
}
if (length(result) == 0) {
log.error("get returned empty blob")
disrupt
}
log.console(`get (fs): ok (${length(result)} bits)`)
// parallel requestor test - fetch multiple files at once
var get_core = cellfs.get('@prosperon/core.cm')
var get_ease = cellfs.get('@prosperon/ease.cm')
parallel([get_color, get_core, get_ease])(function(results, reason) {
if (reason != null) {
log.error(`parallel get failed: ${reason}`)
disrupt
}
if (length(results) != 3) {
log.error(`parallel expected 3 results, got ${length(results)}`)
disrupt
}
log.console(`parallel get: ok (${length(results)} files fetched)`)
// HTTP mount test — network may not be available in test env
cellfs.mount('http://example.com', 'web')
var web_res = cellfs.resolve('@web/')
if (web_res.mount.type != 'http') {
log.error("http mount type is not 'http'")
disrupt
}
log.console("http mount: ok (type=http)")
var get_web = cellfs.get('@web/')
get_web(function(body, reason) {
if (reason != null) {
log.console(`get (http): skipped (${reason})`)
} else {
log.console(`get (http): ok (${length(body)} bits)`)
}
log.console("--- requestor tests passed ---")
log.console("all cellfs tests passed")
$stop()
})
})
})

View File

@@ -1,5 +1,5 @@
// Test pronto functions
// Tests for fallback, parallel, race, sequence, time_limit, requestorize, objectify
// Tests for fallback, parallel, race, sequence
var test_count = 0
@@ -89,49 +89,17 @@ return {
}, 1000)
},
test_time_limit: function() {
log.console("Testing time_limit...")
var slow_req = make_requestor("slow_req", 0.5, true) // takes 0.5s
var timed_req = time_limit(slow_req, 0.2) // 0.2s limit
test_immediate_requestors: function() {
log.console("Testing immediate requestors...")
var req1 = make_requestor("imm1", 0, true)
var req2 = make_requestor("imm2", 0, true)
timed_req(function(result, reason) {
sequence([req1, req2])(function(result, reason) {
if (result != null) {
log.console(`Time limit succeeded: ${result}`)
log.console(`Immediate sequence result: ${result}`)
} else {
log.console(`Time limit failed: ${reason}`)
log.console(`Immediate sequence failed: ${reason}`)
}
}, 100)
},
test_requestorize: function() {
log.console("Testing requestorize...")
var add_one = function(x) { return x + 1 }
var req = requestorize(add_one)
req(function(result, reason) {
if (result != null) {
log.console(`Requestorize result: ${result}`)
} else {
log.console(`Requestorize failed: ${reason}`)
}
}, 42)
},
test_objectify: function() {
log.console("Testing objectify...")
var req_a = make_requestor("obj_req_a", 0.1, true)
var req_b = make_requestor("obj_req_b", 0.1, true)
var req_c = make_requestor("obj_req_c", 0.1, true)
var parallel_obj = objectify(parallel)
var req = parallel_obj({a: req_a, b: req_b, c: req_c})
req(function(result, reason) {
if (result != null) {
log.console(`Objectify result: ${result}`)
} else {
log.console(`Objectify failed: ${reason}`)
}
}, 1000)
}, 0)
}
}

View File

@@ -1,5 +1,6 @@
var shop = use('internal/shop')
return {
file_reload: shop.file_reload
file_reload: shop.file_reload,
reload: shop.module_reload
}