Compare commits
18 Commits
d0bf757d91
...
957b964d9d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
957b964d9d | ||
|
|
fa9d2609b1 | ||
|
|
e38c2f07bf | ||
|
|
ecc1777b24 | ||
|
|
1cfd5b8133 | ||
|
|
9c1141f408 | ||
|
|
696cca530b | ||
|
|
c92a4087a6 | ||
|
|
01637c49b0 | ||
|
|
f9e660ebaa | ||
|
|
4f8fada57d | ||
|
|
adcaa92bea | ||
|
|
fc36707b39 | ||
|
|
7cb8ce7945 | ||
|
|
bb7997a751 | ||
|
|
327b990442 | ||
|
|
51c0a0b306 | ||
|
|
8ac82016dd |
29
build.cm
29
build.cm
@@ -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
164
cellfs.cm
@@ -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
|
||||
|
||||
@@ -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
515
http.cm
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
494
log.ce
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
90
net/socket.c
90
net/socket.c
@@ -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
238
net/tls.c
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
251
tests/cellfs_test.ce
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user