diff --git a/exec/load/dns.js b/exec/load/dns.js new file mode 100644 index 0000000000000000000000000000000000000000..bbe4a374389eb5dd60ef1baa7d489c7e6a599d62 --- /dev/null +++ b/exec/load/dns.js @@ -0,0 +1,634 @@ +/* DNS protocol library + * Uses events, so the calling script *MUST* set js.do_callbacks + * Example usage: + * + * js.do_callbacks = true; + * load('dns.js'); + * + * function handle(resp) + * { + * log(LOG_ERROR, JSON.stringify(resp)); + * exit(0); + * } + * + * dns.resolve('example.com', handle); + * + */ + +require('sockdefs.js', 'SOCK_DGRAM'); + +function DNS(servers) { + if (servers === undefined) + servers = system.name_servers; + var nextid = 0; + var outstanding = {}; + this.sockets = []; + + handle_response = function() { + var resp; + var id; + var offset = 0; + var queries; + var answers; + var nameservers; + var arecords; + var q; + var i; + var ret = {'queries':[], 'answers':[], 'nameservers':[], 'additional':[]}; + var rdata; + var rdlen; + var tmp; + + string_to_int16 = function(str) { + return ((ascii(str[0])<<8) | (ascii(str[1]))); + } + + string_to_int32 = function(str) { + return ((ascii(str[0])<<24) | (ascii(str[1]) << 16) | (ascii(str[1]) << 8) | (ascii(str[1]))); + } + + function get_string(resp, offset) { + var len = ascii(resp[offset]); + return {'len':len + 1, 'string':resp.substr(offset + 1, len)}; + } + + function get_name(resp, offset) { + var len; + var name = ''; + var ret = 0; + var compressed = false; + + do { + len = ascii(resp[offset]); + if (!compressed) + ret++; + offset++; + if (len > 63) { + offset = ((len & 0x3f) << 8) | ascii(resp[offset]); + if (!compressed) + ret++; + compressed = true; + } + else { + if (!compressed) + ret += len; + if (name.length > 0 && len > 0) + name += '.'; + name += resp.substr(offset, len); + offset += len; + } + } while (len != 0); + + return {'len':ret, 'name':name}; + } + + function parse_rdata(type, resp, offset, len) { + var tmp; + var tmp2; + var tmp3; + var tmp4; + + switch(type) { + case 1: // A + return ascii(resp[offset]) + '.' + + ascii(resp[offset + 1]) + '.' + + ascii(resp[offset + 2]) + '.' + + ascii(resp[offset + 3]); + case 2: // NS + return get_name(resp, offset).name; + case 5: // CNAME + return get_name(resp, offset).name; + case 6: // SOA + tmp = {}; + tmp2 = 0; + tmp3 = get_name(resp, offset + tmp2); + tmp.mname = tmp3.name; + tmp2 += tmp3.len; + tmp3 = get_name(resp, offset + tmp2); + tmp.rname = tmp3.name; + tmp2 += tmp3.len; + tmp.serial = string_to_int32(resp.substr(offset + tmp2, 4)); + tmp2 += 4; + tmp.refresh = string_to_int32(resp.substr(offset + tmp2, 4)); + tmp2 += 4; + tmp.retry = string_to_int32(resp.substr(offset + tmp2, 4)); + tmp2 += 4; + tmp.export = string_to_int32(resp.substr(offset + tmp2, 4)); + tmp2 += 4; + tmp.minimum = string_to_int32(resp.substr(offset + tmp2, 4)); + tmp2 += 4; + return tmp; + case 11: // WKS + tmp = {}; + tmp.address = ascii(resp[offset]) + '.' + + ascii(resp[offset + 1]) + '.' + + ascii(resp[offset + 2]) + '.' + + ascii(resp[offset + 3]); + tmp.protocol = ascii(resp[offset + 4]); + tmp2 = 5; + tmp.ports = []; + while (tmp2 < len) { + tmp3 = ascii(resp[offset + tmp2]); + for (tmp4 = 0; tmp4 < 8; tmp4++) { + if (tmp3 & (1 << tmp4)) + tmp.ports.push(8 * (tmp2 - 5) + tmp3); + } + } + return tmp; + case 12: // PTR + return get_name(resp, offset).name; + case 13: // HINFO + tmp = get_string(resp, offset); + return {'cpu':tmp.string, 'os':get_string(resp, offset + tmp.len).string}; + case 15: // MX + tmp = {}; + tmp.preference = string_to_int16(resp.substr(offset, 2)); + tmp.exchange = get_name(resp, offset + 2).name; + return tmp; + case 16: // TXT + tmp = []; + tmp2 = 0; + do { + tmp3 = get_string(resp, offset + tmp2); + tmp.push(tmp3.string); + tmp2 += tmp3.len; + } while (tmp2 < len); + return tmp; + case 28: + return format("%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x", + ascii(resp[offset + 0]), ascii(resp[offset + 1]), + ascii(resp[offset + 2]), ascii(resp[offset + 3]), + ascii(resp[offset + 4]), ascii(resp[offset + 5]), + ascii(resp[offset + 6]), ascii(resp[offset + 7]), + ascii(resp[offset + 8]), ascii(resp[offset + 9]), + ascii(resp[offset + 10]), ascii(resp[offset + 11]), + ascii(resp[offset + 12]), ascii(resp[offset + 13]), + ascii(resp[offset + 14]), ascii(resp[offset + 15]) + ).replace(/(0000:)+/, ':').replace(/(^|:)0{1,3}/g, '$1'); + case 3: // MD + case 4: // MF + case 7: // MB + case 8: // MG + case 9: // MR + case 10: // NULL + case 14: // MINFO + return {'raw':resp.substr(offset, len)}; + } + } + + resp = this.recv(10000); + id = string_to_int16(resp); + if (this.outstanding[id] === undefined) + return false; + + q = this.outstanding[id]; + delete this.outstanding[id]; + + ret.id = id; + ret.response = !!(ascii(resp[2]) & 1); + ret.opcode = (ascii(resp[2]) & 0x1e) >> 1; + ret.authoritative = !!(ascii(resp[2]) & (1<<5)); + ret.truncation = !!(ascii(resp[2]) & (1<<6)); + ret.recusrion = !!(ascii(resp[2]) & (1<<7)); + ret.reserved = ascii(resp[3]) & 7; + ret.rcode = ascii(resp[3] & 0xf1) >> 3; + + queries = string_to_int16(resp.substr(4, 2)); + answers = string_to_int16(resp.substr(6, 2)); + nameservers = string_to_int16(resp.substr(8, 2)); + arecords = string_to_int16(resp.substr(10, 2)); + if (answers === 0) + return false; + js.clearTimeout(q.timeoutid); + offset = 12; + for (i = 0; i < queries; i++) { + rdata = {}; + tmp = get_name(resp, offset); + rdata.name = tmp.name; + offset += tmp.len; + rdata.type = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.class = string_to_int16(resp.substr(offset, 2)); + offset += 2; + ret.queries.push(rdata); + } + + for (i = 0; i < answers; i++) { + rdata = {}; + tmp = get_name(resp, offset); + rdata.name = tmp.name; + offset += tmp.len; + rdata.type = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.class = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.ttl = string_to_int32(resp.substr(offset, 4)); + offset += 4; + rdlen = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.rdata = parse_rdata(rdata.type, resp, offset, rdlen); + offset += rdlen; + ret.answers.push(rdata); + } + + for (i = 0; i < nameservers; i++) { + rdata = {}; + tmp = get_name(resp, offset); + rdata.name = tmp.name; + offset += tmp.len; + rdata.type = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.class = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.ttl = string_to_int32(resp.substr(offset, 4)); + offset += 4; + rdlen = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.rdata = parse_rdata(rdata.type, resp, offset, rdlen); + offset += rdlen; + ret.nameservers.push(rdata); + } + + for (i = 0; i < arecords; i++) { + rdata = {}; + tmp = get_name(resp, offset); + rdata.name = tmp.name; + offset += tmp.len; + rdata.type = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.class = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.ttl = string_to_int32(resp.substr(offset, 4)); + offset += 4; + rdlen = string_to_int16(resp.substr(offset, 2)); + offset += 2; + rdata.rdata = parse_rdata(rdata.type, resp, offset, rdlen); + offset += rdlen; + ret.additional.push(rdata); + } + + q.callback.call(q.thisObj, ret); + return true; + } + + servers.forEach(function(server) { + var sock = new Socket(SOCK_DGRAM, "dns", server.indexOf(':') >= 0); + sock.bind(); + sock.connect(server, 53); + sock.on('read', handle_response); + sock.outstanding = outstanding; + this.sockets.push(sock); + }, this); + + if (this.sockets.length < 1) + throw('Unable to create any sockets'); + + increment_id = function() { + var ret = nextid; + do { + nextid++; + if (nextid > 65535) + nextid = 0; + } while (outstanding[nextid] !== undefined); + return ret; + } + +} + +DNS.types = { + 'A':1, + 'NS':2, + 'MD':3, + 'MF':4, + 'CNAME':5, + 'SOA':6, + 'MB':7, + 'MG':8, + 'MR':9, + 'NULL':10, + 'WKS':11, + 'PTR':12, + 'HINFO':13, + 'MINFO':14, + 'MX':15, + 'TXT':16, + 'AAAA':28 +}; + +DNS.classes = { + 'IN':1, + 'CS':2, + 'CH':3, + 'HS':4 +}; + +DNS.prototype.query = function(queries, /* queryStr, type, class, */callback, thisObj, recursive, timeout, failures, failed) { + var id; + var namebits = {}; + + function int16_to_string(id) { + return ascii((id & 0xff00) >> 8) + ascii(id & 0xff); + } + + function handle_timeout() { + this.failed++; + + delete outstanding[this.id]; + + if (this.failed > 2) + this.callback.call(this); + else + this.obj.query(this.query, this.callback, this.thisObj, this.recursive, this.timeout, this.failures, this.failed); + } + + queries.forEach(function(query) { + if (query.str === undefined) + query.str = 'example.com'; + if (query.type === undefined) + query.type = 'AAAA'; + if (DNS.types[query.type] !== undefined) + query.type = DNS.types[query.type]; + query.type = parseInt(query.type, 10); + if (isNaN(query.type)) { + throw new Error('Invalid type'); + } + if (query.class === undefined) + query.class = 'IN'; + if (DNS.classes[query.class] !== undefined) + query.class = DNS.classes[query.class]; + query.class = parseInt(query.class, 10); + if (isNaN(query.class)) { + throw new Error('Invalid class'); + } + }); + + if (callback === undefined) + return; + if (recursive === undefined) + recursive = true; + if (timeout === undefined) + timeout = 1000; + if (failures === undefined) + failures = 1; + if (failed === undefined) + failed = 0; + + var query = ''; + + // Header + id = increment_id(); + query = int16_to_string(id); + query += ascii(recursive ? 1 : 0); + query += ascii(0); + query += int16_to_string(queries.length); // Questions + query += int16_to_string(0); // Answers + query += int16_to_string(0); // Name Servers + query += int16_to_string(0); // Additional Records + + // Queries + queries.forEach(function(oneQuery) { + var thisname = ''; + var fields = oneQuery.query.split(/\./).reverse(); + var matched; + + fields.forEach(function(field) { + if (field.length > 63) + throw new Error('invalid field in query "'+field+'" (longer than 63 characters)'); + thisname = ascii(field.length) + field + thisname; + if (namebits[thisname] !== undefined && namebits[thisname] > 0) { + matched = {'offset':namebits[thisname], 'length':thisname.length}; + } + else { + namebits[thisname] = 0 - thisname.length; + } + }); + + // Now fix up negative namebits... + Object.keys(namebits).forEach(function(name) { + if (namebits[name] < 0) { + namebits[name] = query.length + thisname.length + namebits[name]; + } + }); + + // And use the match + if (matched !== undefined) { + thisname = thisname.substr(0, thisname.length - matched.length); + thisname = thisname + ascii(0xc0 | ((matched.offset & 0x3f) >> 8)) + ascii(matched.offset & 0xff); + } + else + thisname += ascii(0); + query += thisname; + query += int16_to_string(oneQuery.type); + query += int16_to_string(oneQuery.class); + }, this); + + this.sockets[0].outstanding[id] = {'callback':callback, 'query':queries, 'recursive':recursive, 'timeout':timeout, 'thisObj':thisObj, 'id':id, 'obj':this, 'failed':failed}; + this.sockets[0].outstanding[id].timeoutid = js.setTimeout(handle_timeout, timeout, this.sockets[0].outstanding[id]); + + this.sockets.forEach(function(sock) { + sock.write(query); + }); +} + +DNS.prototype.resolve = function(host, callback, thisObj) +{ + var ctx = {A:{}, AAAA:{}, unique_id:'DNS.prototype.resolve'}; + var final; + var respA; + var respAAAA; + + this.sockets.forEach(function(sock) { + ctx.unique_id += '.'+sock.local_port; + }); + + if (host === undefined) + throw new Error('No host specified'); + + if (thisObj === undefined) + thisObj = this; + + ctx.callback = callback; + ctx.thisObj = thisObj; + + ctx.final = js.addEventListener(ctx.unique_id+'.final', function() { + var ret = []; + this.AAAA.addrs.forEach(function(addr) { + ret.push(addr); + }); + this.A.addrs.forEach(function(addr) { + ret.push(addr); + }); + js.removeEventListener(this.final); + js.removeEventListener(this.respA); + js.removeEventListener(this.respAAAA); + this.callback.call(this.thisObj, ret); + }); + + ctx.respA = js.addEventListener(ctx.unique_id+'.respA', function() { + this.A.done = true; + if (this.AAAA.done) + js.dispatchEvent(this.unique_id + '.final', this); + }); + + ctx.respAAAA = js.addEventListener(ctx.unique_id+'.respAAAA', function() { + this.AAAA.done = true; + if (this.A.done) + js.dispatchEvent(ctx.unique_id + '.final', this); + }); + + function handle_response(resp) { + var rectype; + switch(resp.queries[0].type) { + case DNS.types.A: + rectype = 'A'; + break; + case DNS.types.AAAA: + rectype = 'AAAA'; + break; + }; + if (rectype === undefined) + return; + + this[rectype].addrs = []; + + resp.answers.forEach(function(ans) { + if (resp.queries[0].type != ans.type || resp.queries[0].class != ans.class) + return; + this[rectype].addrs.push(ans.rdata); + }, this); + js.dispatchEvent(this.unique_id + '.resp'+rectype, this); + } + + this.query([{query:host, type:'AAAA'}], handle_response, ctx); + this.query([{query:host, type:'A'}], handle_response, ctx); +} + +DNS.prototype.resolveTypeClass = function(host, type, class, callback, thisObj) +{ + var ctx = {}; + var final; + var respA; + var respAAAA; + + this.sockets.forEach(function(sock) { + ctx.unique_id += '.'+sock.local_port; + }); + + if (host === undefined) + throw new Error('No host specified'); + + if (type === undefined) + throw new Error('No type specified'); + + if (class === undefined) + throw new Error('No class specified'); + + if (callback === undefined) + throw new Error('No callback specified'); + ctx.callback = callback; + + if (thisObj === undefined) + thisObj = this; + ctx.thisObj = thisObj; + + function handle_response(resp) { + ret = []; + if (resp !== undefined && resp.answers !== undefined) { + resp.answers.forEach(function(ans) { + if (resp.queries[0].type != ans.type || resp.queries[0].class != ans.class) + return; + ret.push(ans.rdata); + }); + } + this.callback.call(this.thisObj, ret); + } + + this.query([{query:host, type:type, class:class}], handle_response, ctx); +} + +DNS.prototype.reverse = function(ip, callback, thisObj) +{ + var qstr; + var m; + var a; + var fillpos; + + if (ip === undefined) + throw new Error('No IP specified'); + + if (thisObj === undefined) + thisObj = this; + + // Sure, this doesn't deal with terrible ipv4-mapped representations. Suck it. + m = ip.match(/^(?:::ffff:)?([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i); + if (m !== null) { + // IPv4 Address + if (parseInt(m[1], 10) > 255 || parseInt(m[2], 10) > 255 || parseInt(m[3], 10) > 255 || parseInt(m[4], 10) > 255) + throw new Error('Malformed IP address '+ip); + qstr = m[4] + '.' + m[3] + '.' + m[2] + '.' + m[1] + '.in-addr.arpa'; + } + else { + a = ip.split(/:/); + if (a.length < 3 || a.length > 8) + throw new Error('Malformed IP address '+ip); + if (ip.search(/^[a-fA-F0-9:]+$/) != 0) + throw new Error('Malformed IP address '+ip); + a.forEach(function(piece, idx, arr) { + if (piece !== '') { + while (piece.length < 4) + piece = '0'+piece; + } + arr[idx] = piece; + }); + if (a[0] == '') + a[0] = '0000'; + if (a[a.length - 1] == '') + a[a.length] = '0000'; + while (a.length < 8) { + fillpos = a.indexOf(''); + if (fillpos === -1) + throw('Unable to expand IPv6 address'); + a.splice(fillpos, 0, '0000'); + } + fillpos = a.indexOf(''); + if (fillpos != -1) + a.splice(fillpos, 1, '0000'); + a.reverse(); + qstr = ''; + a.forEach(function(piece) { + qstr += piece[3] + '.' + piece[2] + '.' + piece[1] + '.' + piece[0] + '.'; + }); + qstr += 'ip6.arpa'; + } + + this.resolveTypeClass(qstr, 'PTR', 'IN', callback, thisObj); +} + +DNS.prototype.resolveMX = function(host, callback, thisObj) +{ + var ctx = {callback:callback}; + var qstr; + var m; + var a; + var fillpos; + + if (host === undefined) + throw new Error('No host specified'); + + if (thisObj === undefined) + thisObj = this; + ctx.thisObj = thisObj; + + function handler(resp) { + var ret = []; + resp.sort(function(a, b) { + return a.preference - b.preference; + }); + resp.forEach(function(r) { + ret.push(r.exchange); + }); + this.callback.call(this.thisObj, ret); + } + + this.resolveTypeClass(host, 'MX', 'IN', handler, ctx); +} diff --git a/src/sbbs3/exec.cpp b/src/sbbs3/exec.cpp index f89056069053b115f3d144eb7196a4378e8abdc7..7900c368f3ce61892d06f1040bba01c5184a6dcc 100644 --- a/src/sbbs3/exec.cpp +++ b/src/sbbs3/exec.cpp @@ -683,6 +683,7 @@ long sbbs_t::js_execfile(const char *cmd, const char* startup_dir, JSObject* sco } js_PrepareToExecute(js_cx, js_glob, path, startup_dir, js_scope); JS_ExecuteScript(js_cx, js_scope, js_script, &rval); + js_handle_events(js_cx, &js_callback, &terminated); sys_status &=~ SS_ABORT; JS_GetProperty(js_cx, js_scope, "exit_code", &rval); diff --git a/src/sbbs3/js_console.cpp b/src/sbbs3/js_console.cpp index 0f05db23e494066170cd448aaa5359e8b8fd4f0a..02c0385502f61b5cd770a48cb3f88f552e5806da 100644 --- a/src/sbbs3/js_console.cpp +++ b/src/sbbs3/js_console.cpp @@ -1998,6 +1998,21 @@ js_getlines(JSContext *cx, uintN argc, jsval *arglist) return(JS_TRUE); } +void +js_do_lock_input(JSContext *cx, JSBool lock) +{ + sbbs_t* sbbs; + + if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL) + return; + + if(lock) { + pthread_mutex_lock(&sbbs->input_thread_mutex); + } else { + pthread_mutex_unlock(&sbbs->input_thread_mutex); + } +} + static JSBool js_lock_input(JSContext *cx, uintN argc, jsval *arglist) { @@ -2109,6 +2124,136 @@ js_term_updated(JSContext *cx, uintN argc, jsval *arglist) return JS_TRUE; } +size_t +js_cx_input_pending(JSContext *cx) +{ + sbbs_t* sbbs; + + if ((sbbs = (sbbs_t*)JS_GetContextPrivate(cx)) == NULL) + return 0; + + return sbbs->keybuf_level() + RingBufFull(&sbbs->inbuf); +} + +static JSBool +js_install_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once) +{ + jsval *argv=JS_ARGV(cx, arglist); + js_callback_t* cb; + JSObject *obj=JS_THIS_OBJECT(cx, arglist); + JSFunction *ecb; + char operation[16]; + enum js_event_type et; + size_t slen; + struct js_event_list *ev; + sbbs_t *sbbs; + + if((sbbs=(sbbs_t*)js_GetClassPrivate(cx, JS_THIS_OBJECT(cx, arglist), &js_console_class))==NULL) + return(JS_FALSE); + + if (argc != 2) { + JS_ReportError(cx, "console.on() and console.once() require exactly two parameters"); + return JS_FALSE; + } + ecb = JS_ValueToFunction(cx, argv[1]); + if (ecb == NULL) { + return JS_FALSE; + } + JSVALUE_TO_STRBUF(cx, argv[0], operation, sizeof(operation), &slen); + HANDLE_PENDING(cx, NULL); + if (strcmp(operation, "read") == 0) { + if (once) + et = JS_EVENT_CONSOLE_INPUT_ONCE; + else + et = JS_EVENT_CONSOLE_INPUT; + } + else { + JS_ReportError(cx, "event parameter must be 'read'"); + return JS_FALSE; + } + + cb = &sbbs->js_callback; + if (cb == NULL) { + return JS_FALSE; + } + if (!cb->events_supported) { + JS_ReportError(cx, "events not supported"); + return JS_FALSE; + } + + ev = (struct js_event_list *)malloc(sizeof(*ev)); + if (ev == NULL) { + JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev)); + return JS_FALSE; + } + ev->prev = NULL; + ev->next = cb->events; + if (ev->next) + ev->next->prev = ev; + ev->type = et; + ev->cx = obj; + JS_AddObjectRoot(cx, &ev->cx); + ev->cb = ecb; + ev->id = cb->next_eid++; + ev->data.sock = sbbs->client_socket; + cb->events = ev; + + JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(ev->id)); + return JS_TRUE; +} + +static JSBool +js_clear_console_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once) +{ + jsval *argv=JS_ARGV(cx, arglist); + enum js_event_type et; + char operation[16]; + size_t slen; + + if (argc != 2) { + JS_ReportError(cx, "console.clearOn() and console.clearOnce() require exactly two parameters"); + return JS_FALSE; + } + JSVALUE_TO_STRBUF(cx, argv[0], operation, sizeof(operation), &slen); + HANDLE_PENDING(cx, NULL); + if (strcmp(operation, "read") == 0) { + if (once) + et = JS_EVENT_CONSOLE_INPUT_ONCE; + else + et = JS_EVENT_CONSOLE_INPUT; + } + else { + JS_SET_RVAL(cx, arglist, JSVAL_VOID); + return JS_TRUE; + } + + return js_clear_event(cx, argc, arglist, et); +} + +static JSBool +js_once(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_install_event(cx, argc, arglist, TRUE); +} + +static JSBool +js_clearOnce(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_clear_console_event(cx, argc, arglist, TRUE); +} + +static JSBool +js_on(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_install_event(cx, argc, arglist, FALSE); +} + +static JSBool +js_clearOn(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_clear_console_event(cx, argc, arglist, TRUE); +} + static jsSyncMethodSpec js_console_functions[] = { {"inkey", js_inkey, 0, JSTYPE_STRING, JSDOCSTR("[mode=<tt>K_NONE</tt>] [,timeout=<tt>0</tt>]") ,JSDOCSTR("get a single key with optional <i>timeout</i> in milliseconds (defaults to 0, for no wait).<br>" @@ -2362,6 +2507,22 @@ static jsSyncMethodSpec js_console_functions[] = { ,JSDOCSTR("scroll all current mouse hot-spots by the specific number of rows") ,31800 }, + {"on", js_on, 2, JSTYPE_NUMBER, JSDOCSTR("type, callback") + ,JSDOCSTR("calls callback whenever the condition type specifies is possible. Currently, only 'read' is supported for type. Returns an id suitable for use with clearOn") + ,31802 + }, + {"once", js_once, 2, JSTYPE_NUMBER, JSDOCSTR("type, callback") + ,JSDOCSTR("calls callback the first time the condition type specifies is possible. Currently, only 'read' is supported for type. Returns an id suitable for use with clearOnce") + ,31802 + }, + {"clearOn", js_clearOn, 2, JSTYPE_VOID, JSDOCSTR("type, id") + ,JSDOCSTR("removes a callback installed by on") + ,31802 + }, + {"clearOnce", js_clearOnce, 2, JSTYPE_VOID, JSDOCSTR("type, id") + ,JSDOCSTR("removes a callback installed by once") + ,31802 + }, {0} }; diff --git a/src/sbbs3/js_internal.c b/src/sbbs3/js_internal.c index e13b8a5a9f9555813c3af44114568f114c2b774c..9027fa46b8b6ce3514b71d7ecac1e2d1df9919b5 100644 --- a/src/sbbs3/js_internal.c +++ b/src/sbbs3/js_internal.c @@ -20,7 +20,9 @@ ****************************************************************************/ #include "sbbs.h" +#include "sockwrap.h" #include "js_request.h" +#include "js_socket.h" /* SpiderMonkey: */ #include <jsdbgapi.h> @@ -42,6 +44,7 @@ enum { #endif ,PROP_GLOBAL ,PROP_OPTIONS + ,PROP_KEEPGOING }; static JSBool js_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp) @@ -111,6 +114,12 @@ static JSBool js_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp) case PROP_OPTIONS: *vp = UINT_TO_JSVAL(JS_GetOptions(cx)); break; + case PROP_KEEPGOING: + if (cb->events_supported) + *vp = BOOLEAN_TO_JSVAL(cb->keepGoing); + else + *vp = JSVAL_FALSE; + break; } return(JS_TRUE); @@ -158,6 +167,10 @@ static JSBool js_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict, jsval return JS_FALSE; break; #endif + case PROP_KEEPGOING: + if (cb->events_supported) + JS_ValueToBoolean(cx, *vp, &cb->keepGoing); + break; } return(JS_TRUE); @@ -186,6 +199,7 @@ static jsSyncPropertySpec js_properties[] = { #endif { "global", PROP_GLOBAL, PROP_FLAGS, 314 }, { "options", PROP_OPTIONS, PROP_FLAGS, 31802 }, + { "do_callbacks", PROP_KEEPGOING, JSPROP_ENUMERATE, 31802 }, {0} }; @@ -207,6 +221,7 @@ static char* prop_desc[] = { #endif ,"global (top level) object - <small>READ ONLY</small>" ,"option flags - <small>READ ONLY</small>" + ,"do callbacks after script finishes running" /* New properties go here... */ ,"full path and filename of JS file executed" ,"directory of executed JS file" @@ -690,6 +705,659 @@ static JSBool js_flatten(JSContext *cx, uintN argc, jsval *arglist) return(JS_TRUE); } +static JSBool +js_setTimeout(JSContext *cx, uintN argc, jsval *arglist) +{ + jsval *argv=JS_ARGV(cx, arglist); + struct js_event_list *ev; + js_callback_t* cb; + JSObject *obj=JS_THIS_OBJECT(cx, arglist); + JSFunction *ecb; + uint64_t now = xp_timer() * 1000; + jsdouble timeout; + + if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL) + return(JS_FALSE); + + if (!cb->events_supported) { + JS_ReportError(cx, "events not supported"); + return JS_FALSE; + } + + if (argc < 2) { + JS_ReportError(cx, "js.setTimeout() requires two parameters"); + return JS_FALSE; + } + ecb = JS_ValueToFunction(cx, argv[0]); + if (ecb == NULL) { + return JS_FALSE; + } + if (argc > 2) { + if (!JS_ValueToObject(cx, argv[2], &obj)) + return JS_FALSE; + } + if (!JS_ValueToNumber(cx, argv[1], &timeout)) { + return JS_FALSE; + } + ev = malloc(sizeof(*ev)); + if (ev == NULL) { + JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev)); + return JS_FALSE; + } + ev->prev = NULL; + ev->next = cb->events; + if (ev->next) + ev->next->prev = ev; + ev->type = JS_EVENT_TIMEOUT; + ev->cx = obj; + JS_AddObjectRoot(cx, &ev->cx); + ev->cb = ecb; + ev->data.timeout.end = now + timeout; + ev->id = cb->next_eid++; + cb->events = ev; + + JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(ev->id)); + return JS_TRUE; +} + +JSBool +js_clear_event(JSContext *cx, uintN argc, jsval *arglist, enum js_event_type et) +{ + jsval *argv=JS_ARGV(cx, arglist); + int32 id; + js_callback_t* cb; + struct js_event_list *ev; + struct js_event_list *nev; + JSObject *obj=JS_THIS_OBJECT(cx, arglist); + + JS_SET_RVAL(cx, arglist, JSVAL_VOID); + + if (argc < 1) { + JS_ReportError(cx, "js.clearTimeout() requires an id"); + return JS_FALSE; + } + if (!JS_ValueToInt32(cx, argv[0], &id)) { + return JS_FALSE; + } + if((cb=(js_callback_t*)JS_GetPrivate(cx, obj))==NULL) + return(JS_FALSE); + if (!cb->events_supported) { + JS_ReportError(cx, "events not supported"); + return JS_FALSE; + } + + for (ev = cb->events; ev; ev = nev) { + nev = ev->next; + if (ev->type == et && ev->id == id) { + if (ev->next) + ev->next->prev = ev->prev; + if (ev->prev) + ev->prev->next = ev->next; + else + cb->events = ev->next; + JS_RemoveObjectRoot(cx, &ev->cx); + free(ev); + } + } + + return JS_TRUE; +} + +static JSBool +js_clearTimeout(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_clear_event(cx, argc, arglist, JS_EVENT_TIMEOUT); +} + +static JSBool +js_clearInterval(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_clear_event(cx, argc, arglist, JS_EVENT_INTERVAL); +} + +static JSBool +js_setInterval(JSContext *cx, uintN argc, jsval *arglist) +{ + jsval *argv=JS_ARGV(cx, arglist); + struct js_event_list *ev; + js_callback_t* cb; + JSObject *obj=JS_THIS_OBJECT(cx, arglist); + JSFunction *ecb; + uint64_t now = xp_timer() * 1000; + jsdouble period; + + if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL) + return(JS_FALSE); + + if (!cb->events_supported) { + JS_ReportError(cx, "events not supported"); + return JS_FALSE; + } + + if (argc < 2) { + JS_ReportError(cx, "js.setInterval() requires two parameters"); + return JS_FALSE; + } + ecb = JS_ValueToFunction(cx, argv[0]); + if (ecb == NULL) { + return JS_FALSE; + } + if (!JS_ValueToNumber(cx, argv[1], &period)) { + return JS_FALSE; + } + if (argc > 2) { + if (!JS_ValueToObject(cx, argv[2], &obj)) + return JS_FALSE; + } + ev = malloc(sizeof(*ev)); + if (ev == NULL) { + JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev)); + return JS_FALSE; + } + ev->prev = NULL; + ev->next = cb->events; + if (ev->next) + ev->next->prev = ev; + ev->type = JS_EVENT_INTERVAL; + ev->cx = obj; + JS_AddObjectRoot(cx, &ev->cx); + ev->cb = ecb; + ev->data.interval.last = now; + ev->data.interval.period = period; + ev->id = cb->next_eid++; + cb->events = ev; + + JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(ev->id)); + return JS_TRUE; +} + +static JSBool +js_addEventListener(JSContext *cx, uintN argc, jsval *arglist) +{ + jsval *argv=JS_ARGV(cx, arglist); + char *name; + JSFunction *cbf; + struct js_listener_entry *listener; + JSObject *obj=JS_THIS_OBJECT(cx, arglist); + js_callback_t *cb; + + if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL) + return(JS_FALSE); + + if (argc < 2) { + JS_ReportError(cx, "js.addEventListener() requires exactly two parameters"); + return JS_FALSE; + } + + cbf = JS_ValueToFunction(cx, argv[1]); + if (cbf == NULL) { + return JS_FALSE; + } + + JSVALUE_TO_MSTRING(cx, argv[0], name, NULL); + HANDLE_PENDING(cx, name); + + listener = malloc(sizeof(*listener)); + if (listener == NULL) { + free(name); + JS_ReportError(cx, "error allocating %ul bytes", sizeof(*listener)); + return JS_FALSE; + } + + listener->func = cbf; + listener->id = cb->next_eid++; + listener->next = cb->listeners; + listener->name = name; + cb->listeners = listener; + + JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(listener->id)); + return JS_TRUE; +} + +static JSBool +js_removeEventListener(JSContext *cx, uintN argc, jsval *arglist) +{ + jsval *argv=JS_ARGV(cx, arglist); + int32 id; + struct js_listener_entry *listener; + struct js_listener_entry **plistener; + char *name; + JSObject *obj=JS_THIS_OBJECT(cx, arglist); + js_callback_t *cb; + + if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL) + return(JS_FALSE); + + if (argc < 1) { + JS_ReportError(cx, "js.removeEventListener() requires exactly one parameter"); + return JS_FALSE; + } + + if (JSVAL_IS_INT(argv[0])) { + // Remove by ID + if (!JS_ValueToInt32(cx, argv[0], &id)) + return JS_FALSE; + for (listener = cb->listeners, plistener = &cb->listeners; listener; listener = (*plistener)->next) { + if (listener->id == id) { + (*plistener)->next = listener->next; + free(listener); + } + else + *plistener = listener; + } + } + else { + // Remove by name + JSVALUE_TO_MSTRING(cx, argv[0], name, NULL); + HANDLE_PENDING(cx, name); + for (listener = cb->listeners, plistener = &cb->listeners; listener; listener = (*plistener)->next) { + if (strcmp(listener->name, name) == 0) { + (*plistener)->next = listener->next; + free(listener); + } + else + *plistener = listener; + } + free(name); + } + + JS_SET_RVAL(cx, arglist, JSVAL_VOID); + return JS_TRUE; +} + +static JSBool +js_dispatchEvent(JSContext *cx, uintN argc, jsval *arglist) +{ + jsval *argv=JS_ARGV(cx, arglist); + struct js_listener_entry *listener; + struct js_runq_entry *rqe; + JSObject *obj=JS_THIS_OBJECT(cx, arglist); + char *name; + js_callback_t *cb; + + if((cb=(js_callback_t*)JS_GetPrivate(cx,obj))==NULL) + return(JS_FALSE); + + if (argc < 1) { + JS_ReportError(cx, "js.dispatchEvent() requires a event name"); + return JS_FALSE; + } + + if (argc > 1) { + if (!JSVAL_IS_OBJECT(argv[1])) { + JS_ReportError(cx, "second argument must be an object"); + return JS_FALSE; + } + obj = JSVAL_TO_OBJECT(argv[1]); + } + + JSVALUE_TO_MSTRING(cx, argv[0], name, NULL); + HANDLE_PENDING(cx, name); + + for (listener = cb->listeners; listener; listener = listener->next) { + if (strcmp(name, listener->name) == 0) { + rqe = malloc(sizeof(*rqe)); + if (rqe == NULL) { + JS_ReportError(cx, "error allocating %ul bytes", sizeof(*rqe)); + free(name); + return JS_FALSE; + } + rqe->func = listener->func; + rqe->cx = obj; + JS_AddObjectRoot(cx, &rqe->cx); + rqe->next = NULL; + if (cb->rq_tail != NULL) + cb->rq_tail->next = rqe; + cb->rq_tail = rqe; + if (cb->rq_head == NULL) + cb->rq_head = rqe; + } + } + free(name); + + JS_SET_RVAL(cx, arglist, JSVAL_VOID); + return JS_TRUE; +} + +static void +js_clear_events(JSContext *cx, js_callback_t *cb) +{ + struct js_event_list *ev; + struct js_event_list *nev; + struct js_runq_entry *rq; + struct js_runq_entry *nrq; + struct js_listener_entry *le; + struct js_listener_entry *nle; + + for (ev = cb->events, nev = NULL; ev; ev = nev) { + nev = ev->next; + JS_RemoveObjectRoot(cx, &ev->cx); + free(ev); + } + cb->next_eid = 0; + cb->events = NULL; + + for (rq = cb->rq_head; rq; rq = nrq) { + nrq = rq->next; + JS_RemoveObjectRoot(cx, &rq->cx); + free(rq); + } + cb->rq_head = NULL; + cb->rq_tail = NULL; + + for (le = cb->listeners; le; le = nle) { + nle = le->next; + free(le); + } + cb->listeners = NULL; +} + +JSBool +js_handle_events(JSContext *cx, js_callback_t *cb, volatile int *terminated) +{ + struct js_event_list **head = &cb->events; + int timeout; + int i; + uint64_t now; + struct js_event_list *ev; + struct js_event_list *tev; + struct js_event_list *cev; + jsval rval = JSVAL_NULL; + jsrefcount rc; + JSBool ret = JS_TRUE; + BOOL input_locked = FALSE; +#ifdef PREFER_POLL + struct pollfd *fds; + nfds_t sc; + nfds_t cfd; +#else + fd_set rfds; + fd_set wfds; + struct timeval tv; + SOCKET hsock; +#endif + + if (!cb->events_supported) + return JS_FALSE; + + while (cb->keepGoing && !JS_IsExceptionPending(cx) && cb->events && !*terminated) { + timeout = -1; // Infinity by default... + now = xp_timer() * 1000; + ev = NULL; + tev = NULL; + cev = NULL; +#ifdef PREFER_POLL + fds = NULL; + sc = 0; + cfd = 0; +#else + hsock = 0; +#endif + +#ifdef PREFER_POLL + for (ev = *head; ev; ev = ev->next) { + if (ev->type == JS_EVENT_SOCKET_READABLE || ev->type == JS_EVENT_SOCKET_READABLE_ONCE + || ev->type == JS_EVENT_SOCKET_WRITABLE || ev->type == JS_EVENT_SOCKET_WRITABLE_ONCE + || ev->type == JS_EVENT_SOCKET_CONNECT || ev->type == JS_EVENT_CONSOLE_INPUT + || ev->type == JS_EVENT_CONSOLE_INPUT_ONCE) + sc++; + } + + if (sc) { + fds = calloc(sc, sizeof(*fds)); + if (fds == NULL) { + JS_ReportError(cx, "error allocating %d elements of %ul bytes", sc, sizeof(*fds)); + return JS_FALSE; + } + } +#else + FD_ZERO(&rfds); + FD_ZERO(&wfds); +#endif + + rc = JS_SUSPENDREQUEST(cx); + if (cb->rq_head) + timeout = 0; + for (ev = *head; ev; ev = ev->next) { + switch (ev->type) { + case JS_EVENT_SOCKET_READABLE_ONCE: + case JS_EVENT_SOCKET_READABLE: +#ifdef PREFER_POLL + fds[cfd].fd = ev->data.sock; + fds[cfd].events = POLLIN; + cfd++; +#else + FD_SET(ev->data.sock, &rfds); + if (ev->data.sock > hsock) + hsock = ev->data.sock; +#endif + break; + case JS_EVENT_SOCKET_WRITABLE_ONCE: + case JS_EVENT_SOCKET_WRITABLE: +#ifdef PREFER_POLL + fds[cfd].fd = ev->data.sock; + fds[cfd].events = POLLOUT; + cfd++; +#else + FD_SET(ev->data.sock, &wfds); + if (ev->data.sock > hsock) + hsock = ev->data.sock; +#endif + break; + case JS_EVENT_SOCKET_CONNECT: +#ifdef PREFER_POLL + fds[cfd].fd = ev->data.connect.sv[0]; + fds[cfd].events = POLLIN; + cfd++; +#else + FD_SET(ev->data.connect.sv[0], &rfds); + if (ev->data.sock > hsock) + hsock = ev->data.sock; +#endif + break; + case JS_EVENT_INTERVAL: + // TODO: Handle clock wrapping + if (ev->data.interval.last + ev->data.interval.period <= now) { + timeout = 0; + tev = ev; + } + else { + i = ev->data.interval.last + ev->data.interval.period - now; + if (timeout == -1 || i < timeout) { + timeout = i; + tev = ev; + } + } + break; + case JS_EVENT_TIMEOUT: + // TODO: Handle clock wrapping + if (now >= ev->data.timeout.end) { + timeout = 0; + tev = ev; + } + else { + i = ev->data.timeout.end - now; + if (timeout == -1 || i < timeout) { + timeout = i; + tev = ev; + } + } + break; + case JS_EVENT_CONSOLE_INPUT_ONCE: + case JS_EVENT_CONSOLE_INPUT: + if (!input_locked) + js_do_lock_input(cx, TRUE); + if (js_cx_input_pending(cx) != 0) { + js_do_lock_input(cx, FALSE); + timeout = 0; + cev = ev; + } + else { + input_locked = TRUE; +#ifdef PREFER_POLL + fds[cfd].fd = ev->data.sock; + fds[cfd].events = POLLIN; + cfd++; +#else + FD_SET(ev->data.sock, &rfds); + if (ev->data.sock > hsock) + hsock = ev->data.sock; +#endif + } + break; + } + } + + // TODO: suspend/resume request +#ifdef PREFER_POLL + switch (poll(fds, cfd, timeout)) { +#else + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + switch (select(hsock + 1, &rfds, &wfds, NULL, &tv)) { +#endif + case 0: // Timeout + JS_RESUMEREQUEST(cx, rc); + if (tev && tev->cx && tev->cb) + ev = tev; + else if (cev && cev->cx && cev->cb) + ev = cev; + else { + if (cb->rq_head == NULL) { + JS_ReportError(cx, "Timeout with no event!"); + ret = JS_FALSE; + goto done; + } + } + break; + case -1: // Error... + JS_RESUMEREQUEST(cx, rc); + if (ERROR_VALUE != EINTR) { + JS_ReportError(cx, "poll() returned with error %d", ERROR_VALUE); + ret = JS_FALSE; + goto done; + } + break; + default: // Zero or more sockets ready + JS_RESUMEREQUEST(cx, rc); +#ifdef PREFER_POLL + cfd = 0; +#endif + for (ev = *head; ev; ev = ev->next) { + if (ev->type == JS_EVENT_SOCKET_READABLE || ev->type == JS_EVENT_SOCKET_READABLE_ONCE) { +#ifdef PREFER_POLL + if (fds[cfd].revents & ~(POLLOUT | POLLWRNORM | POLLWRBAND)) { +#else + if (FD_ISSET(ev->data.sock, &rfds)) { +#endif + break; + } +#ifdef PREFER_POLL + cfd++; +#endif + } + else if (ev->type == JS_EVENT_SOCKET_WRITABLE || ev->type == JS_EVENT_SOCKET_WRITABLE_ONCE) { +#ifdef PREFER_POLL + if (fds[cfd].revents & ~(POLLIN | POLLRDNORM | POLLRDBAND | POLLPRI)) { +#else + if (FD_ISSET(ev->data.sock, &wfds)) { +#endif + break; + } +#ifdef PREFER_POLL + cfd++; +#endif + } + else if (ev->type == JS_EVENT_SOCKET_CONNECT) { +#ifdef PREFER_POLL + if (fds[cfd].revents & ~(POLLOUT | POLLWRNORM | POLLWRBAND)) { +#else + if (FD_ISSET(ev->data.connect.sv[0], &wfds)) { +#endif + closesocket(ev->data.connect.sv[0]); + break; + } +#ifdef PREFER_POLL + cfd++; +#endif + } + else if (ev->type == JS_EVENT_CONSOLE_INPUT) { +#ifdef PREFER_POLL + if (fds[cfd].revents & ~(POLLOUT | POLLWRNORM | POLLWRBAND)) { +#else + if (FD_ISSET(ev->data.sock, &wfds)) { +#endif + break; + } +#ifdef PREFER_POLL + cfd++; +#endif + } + } + break; + } + if (ev == NULL) { + if (cb->rq_head == NULL) { + JS_ReportError(cx, "poll() returned ready, but didn't find anything"); + ret = JS_FALSE; + goto done; + } + else { + struct js_runq_entry *rqe = cb->rq_head; + cb->rq_head = rqe->next; + if (cb->rq_head == NULL) + cb->rq_tail = NULL; + ret = JS_CallFunction(cx, rqe->cx, rqe->func, 0, NULL, &rval); + JS_RemoveObjectRoot(cx, &rqe->cx); + free(rqe); + } + } + else { + // Deal with things before running the callback + if (ev->type == JS_EVENT_CONSOLE_INPUT) { + if (input_locked) + js_do_lock_input(cx, FALSE); + } + + ret = JS_CallFunction(cx, ev->cx, ev->cb, 0, NULL, &rval); + + // Clean up/update after call + if (ev->type == JS_EVENT_SOCKET_CONNECT) { + // TODO: call recv() and pass result to callback? + closesocket(ev->data.connect.sv[0]); + } + else if (ev->type == JS_EVENT_INTERVAL) + ev->data.interval.last += ev->data.interval.period; + + // Remove one-shot events + if (ev->type == JS_EVENT_SOCKET_READABLE_ONCE || ev->type == JS_EVENT_SOCKET_WRITABLE_ONCE + || ev->type == JS_EVENT_SOCKET_CONNECT || ev->type == JS_EVENT_TIMEOUT + || ev->type == JS_EVENT_CONSOLE_INPUT_ONCE) { + if (ev->next) + ev->next->prev = ev->prev; + if (ev->prev) + ev->prev->next = ev->next; + else + *head = ev->next; + JS_RemoveObjectRoot(cx, &ev->cx); + free(ev); + } + } + +done: +#ifdef PREFER_POLL + free(fds); +#endif + if (input_locked) + js_do_lock_input(cx, FALSE); + + if (!ret) + break; + } + js_clear_events(cx, cb); + return ret; +} + static jsSyncMethodSpec js_functions[] = { {"eval", js_eval, 0, JSTYPE_UNDEF, JSDOCSTR("script") ,JSDOCSTR("evaluate a JavaScript string in its own (secure) context, returning the result") @@ -732,6 +1400,36 @@ static jsSyncMethodSpec js_functions[] = { "NOTE: Use <tt>js.exec.apply()</tt> if you need to pass a variable number of arguments to the executed script.") ,31702 }, + {"setTimeout", js_setTimeout, 2, JSTYPE_NUMBER, JSDOCSTR("callback, time[, thisObj]") + ,JSDOCSTR("install a timeout. callback() will be called time ms after the function is called. " + "returns an id which can be passed to clearTimeout()") + ,31802 + }, + {"setInterval", js_setInterval, 2, JSTYPE_OBJECT, JSDOCSTR("callback, period[, thisObj]") + ,JSDOCSTR("install a timeout. callback() will be called every period ms after setInterval() is called." + "returns and id which can be passed to clearIntervat()") + ,31802 + }, + {"clearTimeout", js_clearTimeout, 1, JSTYPE_VOID, JSDOCSTR("id") + ,JSDOCSTR("remove a timeout") + ,31802 + }, + {"clearInterval", js_clearInterval, 1, JSTYPE_VOID, JSDOCSTR("id") + ,JSDOCSTR("remove an interval") + ,31802 + }, + {"addEventListener", js_addEventListener, 2, JSTYPE_NUMBER, JSDOCSTR("eventName, callback") + ,JSDOCSTR("Add a listener that is ran after js.dispatchEvent(eventName) is called. Returns an id to be passed to js.removeEventListener") + ,31802 + }, + {"removeEventListener", js_removeEventListener, 1, JSTYPE_VOID, JSDOCSTR("id") + ,JSDOCSTR("Remove listeners added with js.addEventListener(). id can be a string or an id returned by addEventListener. This does not remove already triggered callbacks from the runqueue.") + ,31802 + }, + {"dispatchEvent", js_dispatchEvent, 1, JSTYPE_VOID, JSDOCSTR("eventName [, thisObj]") + ,JSDOCSTR("Add all listeners of eventName to the runqueue. If obj is passed, specifies this in the callback (the js object is used otherwise).") + ,31802 + }, {0} }; diff --git a/src/sbbs3/js_socket.c b/src/sbbs3/js_socket.c index e3f9ec7d1fe41a54a1d43d6a5e11070682e04048..862b9eef521306938d69a4e45dd87b6d8afc8346 100644 --- a/src/sbbs3/js_socket.c +++ b/src/sbbs3/js_socket.c @@ -33,7 +33,7 @@ static void dbprintf(BOOL error, js_socket_private_t* p, char* fmt, ...); static bool do_CryptFlush(js_socket_private_t *p); static int do_cryptAttribute(const CRYPT_CONTEXT session, CRYPT_ATTRIBUTE_TYPE attr, int val); static int do_cryptAttributeString(const CRYPT_CONTEXT session, CRYPT_ATTRIBUTE_TYPE attr, void *val, int len); -static void do_js_close(js_socket_private_t *p, bool finalize); +static void do_js_close(JSContext *cx, js_socket_private_t *p, bool finalize); static BOOL js_DefineSocketOptionsArray(JSContext *cx, JSObject *obj, int type); static JSBool js_accept(JSContext *cx, uintN argc, jsval *arglist); static JSBool js_bind(JSContext *cx, uintN argc, jsval *arglist); @@ -42,6 +42,7 @@ static JSBool js_connect(JSContext *cx, uintN argc, jsval *arglist); static void js_finalize_socket(JSContext *cx, JSObject *obj); static JSBool js_ioctlsocket(JSContext *cx, uintN argc, jsval *arglist); static JSBool js_listen(JSContext *cx, uintN argc, jsval *arglist); +static js_callback_t * js_get_callback(JSContext *cx); static JSBool js_getsockopt(JSContext *cx, uintN argc, jsval *arglist); static JSBool js_peek(JSContext *cx, uintN argc, jsval *arglist); static JSBool js_poll(JSContext *cx, uintN argc, jsval *arglist); @@ -59,13 +60,16 @@ static JSBool js_setsockopt(JSContext *cx, uintN argc, jsval *arglist); static int js_sock_read_check(js_socket_private_t *p, time_t start, int32 timeout, int i); static JSBool js_socket_constructor(JSContext *cx, uintN argc, jsval *arglist); static JSBool js_socket_enumerate(JSContext *cx, JSObject *obj); -static BOOL js_socket_peek_byte(js_socket_private_t *p); +static BOOL js_socket_peek_byte(JSContext *cx, js_socket_private_t *p); static JSBool js_socket_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp); -static ptrdiff_t js_socket_recv(js_socket_private_t *p, void *buf, size_t len, int flags, int timeout); +static ptrdiff_t js_socket_recv(JSContext *cx, js_socket_private_t *p, void *buf, size_t len, int flags, int timeout); static JSBool js_socket_resolve(JSContext *cx, JSObject *obj, jsid id); static int js_socket_sendfilesocket(js_socket_private_t *p, int file, off_t *offset, off_t count); static ptrdiff_t js_socket_sendsocket(js_socket_private_t *p, const void *msg, size_t len, int flush); static JSBool js_socket_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict, jsval *vp); +static JSBool js_install_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once); +static JSBool js_on(JSContext *cx, uintN argc, jsval *arglist); +static JSBool js_once(JSContext *cx, uintN argc, jsval *arglist); static int do_cryptAttribute(const CRYPT_CONTEXT session, CRYPT_ATTRIBUTE_TYPE attr, int val) { @@ -153,12 +157,70 @@ static bool do_CryptFlush(js_socket_private_t *p) return true; } -static void do_js_close(js_socket_private_t *p, bool finalize) +static void +remove_js_socket_event(JSContext *cx, js_callback_t *cb, SOCKET sock) { + struct js_event_list *ev; + struct js_event_list *nev; + + if (!cb->events_supported) { + return; + } + + for (ev = cb->events; ev; ev = nev) { + nev = ev->next; + if (ev->type == JS_EVENT_SOCKET_READABLE || ev->type == JS_EVENT_SOCKET_READABLE_ONCE + || ev->type == JS_EVENT_SOCKET_READABLE || ev->type == JS_EVENT_SOCKET_READABLE_ONCE) { + if (ev->data.sock == sock) { + if (ev->next) + ev->next->prev = ev->prev; + if (ev->prev) + ev->prev->next = ev->next; + else + cb->events = ev->next; + JS_RemoveObjectRoot(cx, &ev->cx); + free(ev); + } + } + else if(ev->type == JS_EVENT_SOCKET_CONNECT) { + if (ev->data.connect.sock == sock) { + if (ev->next) + ev->next->prev = ev->prev; + if (ev->prev) + ev->prev->next = ev->next; + else + cb->events = ev->next; + closesocket(ev->data.connect.sv[0]); + JS_RemoveObjectRoot(cx, &ev->cx); + free(ev); + } + } + } +} + +static void do_js_close(JSContext *cx, js_socket_private_t *p, bool finalize) +{ + size_t i; + if(p->session != -1) { cryptDestroySession(p->session); p->session=-1; } + + // Delete any event handlers for the socket + if (p->js_cb) { + if (p->set) { + for (i = 0; i < p->set->sock_count; i++) { + if (p->set->socks[i].sock != INVALID_SOCKET) + remove_js_socket_event(cx, p->js_cb, p->set->socks[i].sock); + } + } + else { + if (p->sock != INVALID_SOCKET) + remove_js_socket_event(cx, p->js_cb, p->sock); + } + } + if(p->sock==INVALID_SOCKET) { p->is_connected = FALSE; return; @@ -171,18 +233,19 @@ static void do_js_close(js_socket_private_t *p, bool finalize) if (!finalize) shutdown(p->sock, SHUT_RDWR); } + // This is a lie for external sockets... don't tell anyone. p->sock = INVALID_SOCKET; p->is_connected = FALSE; } -static BOOL js_socket_peek_byte(js_socket_private_t *p) +static BOOL js_socket_peek_byte(JSContext *cx, js_socket_private_t *p) { if (do_cryptAttribute(p->session, CRYPT_OPTION_NET_READTIMEOUT, 0) != CRYPT_OK) return FALSE; if (p->peeked) return TRUE; - if (js_socket_recv(p, &p->peeked_byte, 1, 0, 0) == 1) { + if (js_socket_recv(cx, p, &p->peeked_byte, 1, 0, 0) == 1) { p->peeked = TRUE; return TRUE; } @@ -192,7 +255,7 @@ static BOOL js_socket_peek_byte(js_socket_private_t *p) /* Returns > 0 upon successful data received (even if there was an error or disconnection) */ /* Returns -1 upon error (and no data received) */ /* Returns 0 upon timeout or disconnection (and no data received) */ -static ptrdiff_t js_socket_recv(js_socket_private_t *p, void *buf, size_t len, int flags, int timeout) +static ptrdiff_t js_socket_recv(JSContext *cx, js_socket_private_t *p, void *buf, size_t len, int flags, int timeout) { ptrdiff_t total=0; int copied,ret; @@ -204,7 +267,7 @@ static ptrdiff_t js_socket_recv(js_socket_private_t *p, void *buf, size_t len, i return total; if (p->session != -1) { if (flags & MSG_PEEK) - js_socket_peek_byte(p); + js_socket_peek_byte(cx, p); if (p->peeked) { *(uint8_t *)buf = p->peeked_byte; buf=((uint8_t *)buf) + 1; @@ -240,7 +303,7 @@ static ptrdiff_t js_socket_recv(js_socket_private_t *p, void *buf, size_t len, i ret = 0; else if (status != CRYPT_ERROR_COMPLETE) { GCES(ret, p, estr, "popping data"); - do_js_close(p, false); + do_js_close(cx, p, false); } } } @@ -392,7 +455,7 @@ static void js_finalize_socket(JSContext *cx, JSObject *obj) if((p=(js_socket_private_t*)JS_GetPrivate(cx,obj))==NULL) return; - do_js_close(p, true); + do_js_close(cx, p, true); if(!p->external) free(p->set); @@ -420,7 +483,7 @@ js_close(JSContext *cx, uintN argc, jsval *arglist) } rc=JS_SUSPENDREQUEST(cx); - do_js_close(p, false); + do_js_close(cx, p, false); dbprintf(FALSE, p, "closed"); JS_RESUMEREQUEST(cx, rc); @@ -809,6 +872,144 @@ js_accept(JSContext *cx, uintN argc, jsval *arglist) return(JS_TRUE); } +struct js_connect_event_args { + SOCKET sv[2]; + SOCKET sock; + int socktype; + char *host; + BOOL nonblocking; + ushort port; +}; + +static void +js_connect_event_thread(void *args) +{ + struct js_connect_event_args *a = args; + struct addrinfo hints,*res,*cur; + int result; + ulong val; + char sresult; + + SetThreadName("sbbs/jsConnect"); + memset(&hints, 0, sizeof(hints)); + hints.ai_socktype = a->socktype; + hints.ai_flags = AI_ADDRCONFIG; + result = getaddrinfo(a->host, NULL, &hints, &res); + if(result != 0) + goto done; + /* always set to blocking here */ + val = 0; + ioctlsocket(a->sock, FIONBIO, &val); + result = SOCKET_ERROR; + for(cur=res; cur != NULL; cur=cur->ai_next) { + inet_setaddrport((void *)cur->ai_addr, a->port); + + result = connect(a->sock, cur->ai_addr, cur->ai_addrlen); + if(result == 0) + break; + } + sresult = result; + /* Restore original setting here */ + ioctlsocket(a->sock,FIONBIO,(ulong*)&(a->nonblocking)); + send(a->sv[1], &sresult, 1, 0); + +done: + closesocket(a->sv[1]); + free(a); +} + +static JSBool +js_connect_event(JSContext *cx, uintN argc, jsval *arglist, js_socket_private_t *p, ushort port, JSObject *obj) +{ + SOCKET sv[2]; + struct js_event_list *ev; + JSFunction *ecb; + js_callback_t *cb = js_get_callback(cx); + struct js_connect_event_args *args; + jsval *argv=JS_ARGV(cx, arglist); + + if (p->sock == INVALID_SOCKET) { + JS_ReportError(cx, "invalid socket"); + return JS_FALSE; + } + + if (cb == NULL) { + return JS_FALSE; + } + + if (!cb->events_supported) { + JS_ReportError(cx, "events not supported"); + return JS_FALSE; + } + + ecb = JS_ValueToFunction(cx, argv[2]); + if (ecb == NULL) { + return JS_FALSE; + } + + // Create socket pair... +#ifdef _WIN32 + if (socketpair(AF_INET, SOCK_STREAM, 0, sv) == -1) { +#else + if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1) { +#endif + JS_ReportError(cx, "Error %d creating socket pair", ERROR_VALUE); + return JS_FALSE; + } + + // Create event + ev = malloc(sizeof(*ev)); + if (ev == NULL) { + JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev)); + closesocket(sv[0]); + closesocket(sv[1]); + return JS_FALSE; + } + ev->prev = NULL; + ev->next = cb->events; + if (ev->next) + ev->next->prev = ev; + ev->type = JS_EVENT_SOCKET_CONNECT; + ev->cx = obj; + JS_AddObjectRoot(cx, &ev->cx); + ev->cb = ecb; + ev->data.connect.sv[0] = sv[0]; + ev->data.connect.sv[1] = sv[1]; + ev->data.sock = p->sock; + ev->id = cb->next_eid++; + p->js_cb = cb; + cb->events = ev; + + // Start thread + args = malloc(sizeof(*args)); + if (args == NULL) { + JS_ReportError(cx, "error allocating %lu bytes", sizeof(*args)); + closesocket(sv[0]); + closesocket(sv[1]); + return JS_FALSE; + } + args->sv[0] = sv[0]; + args->sv[1] = sv[1]; + args->sock = p->sock; + args->socktype = p->type; + args->host = strdup(p->hostname); + args->port = port; + args->nonblocking = p->nonblocking; + if (args->host == NULL) { + JS_ReportError(cx, "error duplicating hostname"); + closesocket(sv[0]); + closesocket(sv[1]); + free(args); + return JS_FALSE; + } + _beginthread(js_connect_event_thread, 0 /* Can be smaller... */, args); + + // Success! + p->is_connected = TRUE; + JS_SET_RVAL(cx, arglist, JSVAL_TRUE); + return JS_TRUE; +} + static JSBool js_connect(JSContext *cx, uintN argc, jsval *arglist) { @@ -834,6 +1035,13 @@ js_connect(JSContext *cx, uintN argc, jsval *arglist) JSSTRING_TO_MSTRING(cx, str, p->hostname, NULL); port = js_port(cx,argv[1],p->type); rc=JS_SUSPENDREQUEST(cx); + + if (argc > 2 && JSVAL_IS_OBJECT(argv[2]) && JS_ObjectIsFunction(cx, JSVAL_TO_OBJECT(argv[2]))) { + JSBool bgr = js_connect_event(cx, argc, arglist, p, port, obj); + JS_RESUMEREQUEST(cx, rc); + return bgr; + } + dbprintf(FALSE, p, "resolving hostname: %s", p->hostname); memset(&hints, 0, sizeof(hints)); @@ -1201,7 +1409,7 @@ js_recv(JSContext *cx, uintN argc, jsval *arglist) } rc=JS_SUSPENDREQUEST(cx); - len = js_socket_recv(p,buf,len,0,timeout); + len = js_socket_recv(cx,p,buf,len,0,timeout); JS_RESUMEREQUEST(cx, rc); if(len<0) { p->last_error=ERROR_VALUE; @@ -1386,7 +1594,7 @@ js_peek(JSContext *cx, uintN argc, jsval *arglist) } rc=JS_SUSPENDREQUEST(cx); if(p->session==-1) - len = js_socket_recv(p,buf,len,MSG_PEEK,120); + len = js_socket_recv(cx, p,buf,len,MSG_PEEK,120); else len=0; JS_RESUMEREQUEST(cx, rc); @@ -1494,7 +1702,7 @@ js_recvline(JSContext *cx, uintN argc, jsval *arglist) } } - if((got=js_socket_recv(p, &ch, 1, 0, i?1:timeout))!=1) { + if((got=js_socket_recv(cx, p, &ch, 1, 0, i?1:timeout))!=1) { if(p->session == -1) p->last_error = ERROR_VALUE; if (i == 0) { // no data received @@ -1555,18 +1763,18 @@ js_recvbin(JSContext *cx, uintN argc, jsval *arglist) rc=JS_SUSPENDREQUEST(cx); switch(size) { case sizeof(BYTE): - if((rd=js_socket_recv(p,&b,size,MSG_WAITALL,120))==size) + if((rd=js_socket_recv(cx, p,&b,size,MSG_WAITALL,120))==size) JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(b)); break; case sizeof(WORD): - if((rd=js_socket_recv(p,(BYTE*)&w,size,MSG_WAITALL,120))==size) { + if((rd=js_socket_recv(cx, p,(BYTE*)&w,size,MSG_WAITALL,120))==size) { if(p->network_byte_order) w=ntohs(w); JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(w)); } break; case sizeof(DWORD): - if((rd=js_socket_recv(p,(BYTE*)&l,size,MSG_WAITALL,120))==size) { + if((rd=js_socket_recv(cx, p,(BYTE*)&l,size,MSG_WAITALL,120))==size) { if(p->network_byte_order) l=ntohl(l); JS_SET_RVAL(cx, arglist, UINT_TO_JSVAL(l)); @@ -1821,6 +2029,179 @@ js_poll(JSContext *cx, uintN argc, jsval *arglist) return(JS_TRUE); } +static js_callback_t * +js_get_callback(JSContext *cx) +{ + JSObject* scope = JS_GetScopeChain(cx); + JSObject* pscope; + jsval val = JSVAL_NULL; + JSObject *pjs_obj; + + pscope = scope; + while ((!JS_LookupProperty(cx, pscope, "js", &val) || val==JSVAL_VOID || !JSVAL_IS_OBJECT(val)) && pscope != NULL) { + pscope = JS_GetParent(cx, pscope); + if (pscope == NULL) { + JS_ReportError(cx, "Walked to global, no js object!"); + return NULL; + } + } + pjs_obj = JSVAL_TO_OBJECT(val); + return JS_GetPrivate(cx, pjs_obj); +} + +static void +js_install_one_socket_event(JSContext *cx, JSObject *obj, JSFunction *ecb, js_callback_t *cb, js_socket_private_t *p, SOCKET sock, enum js_event_type et) +{ + struct js_event_list *ev; + + ev = malloc(sizeof(*ev)); + if (ev == NULL) { + JS_ReportError(cx, "error allocating %lu bytes", sizeof(*ev)); + return; + } + ev->prev = NULL; + ev->next = cb->events; + if (ev->next) + ev->next->prev = ev; + ev->type = et; + ev->cx = obj; + JS_AddObjectRoot(cx, &ev->cx); + ev->cb = ecb; + ev->data.sock = sock; + ev->id = cb->next_eid; + p->js_cb = cb; + cb->events = ev; +} + +static JSBool +js_install_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once) +{ + jsval *argv=JS_ARGV(cx, arglist); + js_callback_t* cb; + JSObject *obj=JS_THIS_OBJECT(cx, arglist); + JSFunction *ecb; + js_socket_private_t* p; + size_t i; + char operation[16]; + enum js_event_type et; + size_t slen; + + if((p=(js_socket_private_t*)js_GetClassPrivate(cx, obj, &js_socket_class))==NULL) { + return(JS_FALSE); + } + + if (argc != 2) { + JS_ReportError(cx, "js.on() and js.once() require exactly two parameters"); + return JS_FALSE; + } + ecb = JS_ValueToFunction(cx, argv[1]); + if (ecb == NULL) { + return JS_FALSE; + } + JSVALUE_TO_STRBUF(cx, argv[0], operation, sizeof(operation), &slen); + HANDLE_PENDING(cx, NULL); + if (strcmp(operation, "read") == 0) { + if (once) + et = JS_EVENT_SOCKET_READABLE_ONCE; + else + et = JS_EVENT_SOCKET_READABLE; + } + else if (strcmp(operation, "write") == 0) { + if (once) + et = JS_EVENT_SOCKET_WRITABLE_ONCE; + else + et = JS_EVENT_SOCKET_WRITABLE; + } + else { + JS_ReportError(cx, "event parameter must be 'read' or 'write'"); + return JS_FALSE; + } + + cb = js_get_callback(cx); + if (cb == NULL) { + return JS_FALSE; + } + if (!cb->events_supported) { + JS_ReportError(cx, "events not supported"); + return JS_FALSE; + } + + + if (p->set) { + for (i = 0; i < p->set->sock_count; i++) { + if (p->set->socks[i].sock != INVALID_SOCKET) + js_install_one_socket_event(cx, obj, ecb, cb, p, p->set->socks[i].sock, et); + } + } + else { + if (p->sock != INVALID_SOCKET) + js_install_one_socket_event(cx, obj, ecb, cb, p, p->sock, et); + } + JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(cb->next_eid)); + cb->next_eid++; + + if (JS_IsExceptionPending(cx)) + return JS_FALSE; + return JS_TRUE; +} + +static JSBool +js_clear_socket_event(JSContext *cx, uintN argc, jsval *arglist, BOOL once) +{ + jsval *argv=JS_ARGV(cx, arglist); + enum js_event_type et; + char operation[16]; + size_t slen; + + if (argc != 2) { + JS_ReportError(cx, "js.clearOn() and js.clearOnce() require exactly two parameters"); + return JS_FALSE; + } + JSVALUE_TO_STRBUF(cx, argv[0], operation, sizeof(operation), &slen); + HANDLE_PENDING(cx, NULL); + if (strcmp(operation, "read") == 0) { + if (once) + et = JS_EVENT_SOCKET_READABLE_ONCE; + else + et = JS_EVENT_SOCKET_READABLE; + } + else if (strcmp(operation, "write") == 0) { + if (once) + et = JS_EVENT_SOCKET_WRITABLE_ONCE; + else + et = JS_EVENT_SOCKET_WRITABLE; + } + else { + JS_SET_RVAL(cx, arglist, JSVAL_VOID); + return JS_TRUE; + } + + return js_clear_event(cx, argc, arglist, et); +} + +static JSBool +js_once(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_install_event(cx, argc, arglist, TRUE); +} + +static JSBool +js_clearOnce(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_clear_socket_event(cx, argc, arglist, TRUE); +} + +static JSBool +js_on(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_install_event(cx, argc, arglist, FALSE); +} + +static JSBool +js_clearOn(JSContext *cx, uintN argc, jsval *arglist) +{ + return js_clear_socket_event(cx, argc, arglist, TRUE); +} /* Socket Object Properties */ enum { @@ -1994,7 +2375,7 @@ static JSBool js_socket_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict cryptDestroySession(p->session); p->session=-1; ioctlsocket(p->sock,FIONBIO,(ulong*)&(p->nonblocking)); - do_js_close(p, false); + do_js_close(cx, p, false); } } } @@ -2003,7 +2384,7 @@ static JSBool js_socket_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict cryptDestroySession(p->session); p->session=-1; ioctlsocket(p->sock,FIONBIO,(ulong*)&(p->nonblocking)); - do_js_close(p, false); + do_js_close(cx, p, false); } } JS_RESUMEREQUEST(cx, rc); @@ -2069,7 +2450,7 @@ static JSBool js_socket_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp) if (p->peeked) rd = TRUE; else if (p->session != -1) - rd = js_socket_peek_byte(p); + rd = js_socket_peek_byte(cx, p); else socket_check(p->sock,&rd,NULL,0); } @@ -2082,7 +2463,7 @@ static JSBool js_socket_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp) } cnt=0; if (p->session != -1) { - if (js_socket_peek_byte(p)) + if (js_socket_peek_byte(cx, p)) *vp=DOUBLE_TO_JSVAL((double)1); else *vp = JSVAL_ZERO; @@ -2296,6 +2677,22 @@ static jsSyncMethodSpec js_socket_functions[] = { "default timeout value is 0.0 seconds (immediate timeout)") ,310 }, + {"on", js_on, 2, JSTYPE_NUMBER, JSDOCSTR("('read' | 'write'), callback") + ,JSDOCSTR("execute callback whenever socket is readable/writable. Returns an id to be passed to js.clearOn()") + ,31802 + }, + {"once", js_once, 2, JSTYPE_NUMBER, JSDOCSTR("('read' | 'write'), callback") + ,JSDOCSTR("execute callback next time socket is readable/writable Returns and id to be passed to js.clearOnce()") + ,31802 + }, + {"clearOn", js_clearOn, 2, JSTYPE_NUMBER, JSDOCSTR("('read' | 'write'), id") + ,JSDOCSTR("remove callback installed by Socket.on()") + ,31802 + }, + {"clearOnce", js_clearOnce, 2, JSTYPE_NUMBER, JSDOCSTR("('read' | 'write'), id") + ,JSDOCSTR("remove callback installed by Socket.once()") + ,31802 + }, {0} }; diff --git a/src/sbbs3/js_socket.h b/src/sbbs3/js_socket.h index 2944f252b8e86e81c41baa052aa4ab6e1d20d79e..8e5acce615d6cc0bed466172d073a9c8f8cc4142 100644 --- a/src/sbbs3/js_socket.h +++ b/src/sbbs3/js_socket.h @@ -25,6 +25,7 @@ typedef struct char peeked_byte; BOOL peeked; uint16_t local_port; + js_callback_t *js_cb; } js_socket_private_t; #ifdef __cplusplus diff --git a/src/sbbs3/js_system.c b/src/sbbs3/js_system.c index da0d04e140f19e4bcfc04fd26497a0894d0ad781..d5583c9a868da703cf3e3414a1831351655e8ad2 100644 --- a/src/sbbs3/js_system.c +++ b/src/sbbs3/js_system.c @@ -103,8 +103,9 @@ enum { ,SYS_PROP_TEMP_PATH ,SYS_PROP_CMD_SHELL - /* last */ ,SYS_PROP_LOCAL_HOSTNAME + /* last */ + ,SYS_PROP_NAME_SERVERS }; static JSBool js_system_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp) @@ -116,6 +117,10 @@ static JSBool js_system_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp) JSString* js_str; ulong val; jsrefcount rc; + JSObject *robj; + jsval jval; + str_list_t list; + int i; js_system_private_t* sys; if((sys = (js_system_private_t*)js_GetClassPrivate(cx,obj,&js_system_class))==NULL) @@ -321,6 +326,23 @@ static JSBool js_system_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp) JS_RESUMEREQUEST(cx, rc); p=str; break; + case SYS_PROP_NAME_SERVERS: + rc=JS_SUSPENDREQUEST(cx); + robj = JS_NewArrayObject(cx, 0, NULL); + if (robj == NULL) + return JS_FALSE; + *vp = OBJECT_TO_JSVAL(robj); + list = getNameServerList(); + if (list != NULL) { + for (i = 0; list[i]; i++) { + jval = STRING_TO_JSVAL(JS_NewStringCopyZ(cx, list[i])); + if (!JS_SetElement(cx, robj, i, &jval)) + break; + } + } + freeNameServerList(list); + JS_RESUMEREQUEST(cx, rc); + break; } if(p!=NULL) { /* string property */ @@ -440,8 +462,9 @@ static jsSyncPropertySpec js_system_properties[] = { { "clock_ticks_per_second", SYS_PROP_CLOCK_PER_SEC ,SYSOBJ_FLAGS, 311 }, { "timer", SYS_PROP_TIMER ,SYSOBJ_FLAGS, 314 }, - /* last */ { "local_host_name", SYS_PROP_LOCAL_HOSTNAME ,SYSOBJ_FLAGS, 311 }, + { "name_servers", SYS_PROP_NAME_SERVERS,SYSOBJ_FLAGS, 31802 }, + /* last */ {0} }; @@ -515,8 +538,9 @@ static char* sys_prop_desc[] = { ,"number of clock ticks per second" ,"high-resolution timer, in seconds (fractional seconds supported)" - /* INSERT new tabled properties here */ ,"private host name that uniquely identifies this system on the local network" + ,"array of nameservers in use by the system" + /* INSERT new tabled properties here */ /* Manually created (non-tabled) properties */ ,"public host name that uniquely identifies this system on the Internet (usually the same as <i>system.inet_addr</i>)" diff --git a/src/sbbs3/jsdoor.c b/src/sbbs3/jsdoor.c index 92bff87b97f688ff51077d9858f4aaa1ab0571ed..d709c5514a5da449a7c059f87266032fee197b09 100644 --- a/src/sbbs3/jsdoor.c +++ b/src/sbbs3/jsdoor.c @@ -53,6 +53,16 @@ scfg_t scfg; +void js_do_lock_input(JSContext *cx, JSBool lock) +{ + return; +} + +size_t js_cx_input_pending(JSContext *cx) +{ + return 0; +} + void* DLLCALL js_GetClassPrivate(JSContext *cx, JSObject *obj, JSClass* cls) { void *ret = JS_GetInstancePrivate(cx, obj, cls, NULL); diff --git a/src/sbbs3/jsexec.c b/src/sbbs3/jsexec.c index e60885ea196ada646f2554b636a39ceff0ae6978..ff7a96a6b17dd2c42462f1cbb8bcbbb0ea67a2e9 100644 --- a/src/sbbs3/jsexec.c +++ b/src/sbbs3/jsexec.c @@ -1078,7 +1078,11 @@ long js_exec(const char *fname, const char* buf, char** args) if (abort) { result = EXIT_FAILURE; } else { + cb.keepGoing = FALSE; + cb.events_supported = TRUE; exec_result = JS_ExecuteScript(js_cx, js_glob, js_script, &rval); + js_handle_events(js_cx, &cb, &terminated); + char *p; if(buf != NULL) { JSVALUE_TO_MSTRING(js_cx, rval, p, NULL); @@ -1213,6 +1217,7 @@ int main(int argc, char **argv, char** env) cb.yield_interval=JAVASCRIPT_YIELD_INTERVAL; cb.gc_interval=JAVASCRIPT_GC_INTERVAL; cb.auto_terminate=TRUE; + cb.events = NULL; DESCRIBE_COMPILER(compiler); diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp index 090ef481e1ae3f6ad5d23ec855e6203854aacb8f..ed2d051a45ff276bf89875fe4f21becff2dc0383 100644 --- a/src/sbbs3/main.cpp +++ b/src/sbbs3/main.cpp @@ -1285,6 +1285,7 @@ JSContext* sbbs_t::js_init(JSRuntime** runtime, JSObject** glob, const char* des js_callback.yield_interval = startup->js.yield_interval; js_callback.terminated = &terminated; js_callback.auto_terminate = TRUE; + js_callback.events_supported = TRUE; bool success=false; bool rooted=false; @@ -1887,16 +1888,9 @@ void input_thread(void *arg) while(sbbs->online && sbbs->client_socket!=INVALID_SOCKET && node_socket[sbbs->cfg.node_num-1]!=INVALID_SOCKET) { - if(pthread_mutex_lock(&sbbs->input_thread_mutex)!=0) - sbbs->errormsg(WHERE,ERR_LOCK,"input_thread_mutex",0); - #ifdef _WIN32 // No spy sockets - if (!socket_readable(sbbs->client_socket, 1000)) { - if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0) - sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0); - YIELD(); /* This kludge is necessary on some Linux distros */ - continue; /* to allow other threads to lock the input_thread_mutex */ - } + if (!socket_readable(sbbs->client_socket, 1000)) + continue; #else #ifdef PREFER_POLL fds[0].fd = sbbs->client_socket; @@ -1910,22 +1904,15 @@ void input_thread(void *arg) nfds++; } - if (poll(fds, nfds, 1000) < 1) { - if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0) - sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0); - YIELD(); /* This kludge is necessary on some Linux distros */ - continue; /* to allow other threads to lock the input_thread_mutex */ - } + if (poll(fds, nfds, 1000) < 1) + continue; #else #error Spy sockets without poll() was removed in commit 3971ef4dcc3db19f400a648b6110718e56a64cf3 #endif #endif - if(sbbs->client_socket==INVALID_SOCKET) { - if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0) - sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0); + if(sbbs->client_socket==INVALID_SOCKET) break; - } /* ^ ^ * \______ ______/ @@ -1949,22 +1936,17 @@ void input_thread(void *arg) close_socket(uspy_socket[sbbs->cfg.node_num-1]); lprintf(LOG_NOTICE,"Closing local spy socket: %d",uspy_socket[sbbs->cfg.node_num-1]); uspy_socket[sbbs->cfg.node_num-1]=INVALID_SOCKET; - if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0) - sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0); continue; } sock=uspy_socket[sbbs->cfg.node_num-1]; } else { - if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0) - sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0); continue; } #else #error Spy sockets without poll() was removed in commit 3971ef4dcc3db19f400a648b6110718e56a64cf3 #endif #endif - rd=RingBufFree(&sbbs->inbuf); if(rd==0) { // input buffer full @@ -1974,16 +1956,16 @@ void input_thread(void *arg) while((rd=RingBufFree(&sbbs->inbuf))==0 && time(NULL)-start<5) { YIELD(); } - if(rd==0) { /* input buffer still full */ - if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0) - sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0); + if(rd==0) /* input buffer still full */ continue; - } } if(rd > (int)sizeof(inbuf)) rd=sizeof(inbuf); + if(pthread_mutex_lock(&sbbs->input_thread_mutex)!=0) + sbbs->errormsg(WHERE,ERR_LOCK,"input_thread_mutex",0); + #ifdef USE_CRYPTLIB if(sbbs->ssh_mode && sock==sbbs->client_socket) { int err; @@ -2015,6 +1997,9 @@ void input_thread(void *arg) if(pthread_mutex_unlock(&sbbs->input_thread_mutex)!=0) sbbs->errormsg(WHERE,ERR_UNLOCK,"input_thread_mutex",0); + if (rd == 0 && !socket_recvdone(sock, 0)) + continue; + if(rd == SOCKET_ERROR) { #ifdef __unix__ @@ -2163,10 +2148,8 @@ void passthru_thread(void* arg) thread_up(FALSE /* setuid */); while(sbbs->online && sbbs->passthru_socket!=INVALID_SOCKET && !terminate_server) { - if (!socket_readable(sbbs->passthru_socket, 1000)) { - YIELD(); /* This kludge is necessary on some Linux distros */ - continue; /* to allow other threads to lock the input_thread_mutex */ - } + if (!socket_readable(sbbs->passthru_socket, 1000)) + continue; if(sbbs->passthru_socket==INVALID_SOCKET) break; diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h index ff71c7fba647e17a3fa9be3b257ddc56a36a7178..5063311df440b88ee1cbef927dae590c99a139d2 100644 --- a/src/sbbs3/sbbs.h +++ b/src/sbbs3/sbbs.h @@ -297,6 +297,82 @@ extern int thread_suid_broken; /* NPTL is no longer broken */ #include "getmail.h" #include "msg_id.h" +#if defined(JAVASCRIPT) +enum js_event_type { + JS_EVENT_SOCKET_READABLE_ONCE, + JS_EVENT_SOCKET_READABLE, + JS_EVENT_SOCKET_WRITABLE_ONCE, + JS_EVENT_SOCKET_WRITABLE, + JS_EVENT_SOCKET_CONNECT, + JS_EVENT_INTERVAL, + JS_EVENT_TIMEOUT, + JS_EVENT_CONSOLE_INPUT_ONCE, + JS_EVENT_CONSOLE_INPUT +}; + +struct js_event_interval { + uint64_t last; // The tick the last event should have triggered at + uint64_t period; +}; + +struct js_event_timeout { + uint64_t end; // Time the timeout expires +}; + +struct js_event_connect { + SOCKET sv[2]; + SOCKET sock; +}; + +struct js_event_list { + struct js_event_list *prev; + struct js_event_list *next; + JSFunction *cb; + JSObject *cx; + union { + SOCKET sock; + struct js_event_connect connect; + struct js_event_interval interval; + struct js_event_timeout timeout; + } data; + int32 id; + enum js_event_type type; +}; + +struct js_runq_entry { + JSFunction *func; + JSObject *cx; + struct js_runq_entry *next; +}; + +struct js_listener_entry { + char *name; + JSFunction *func; + int32 id; + struct js_listener_entry *next; +}; + +typedef struct js_callback { + struct js_event_list *events; + struct js_runq_entry *rq_head; + struct js_runq_entry *rq_tail; + struct js_listener_entry *listeners; + volatile BOOL* terminated; + struct js_callback *parent_cb; + uint32_t counter; + uint32_t limit; + uint32_t yield_interval; + uint32_t gc_interval; + uint32_t gc_attempts; + uint32_t offline_counter; + int32 next_eid; + BOOL auto_terminate; + BOOL keepGoing; + BOOL bg; + BOOL events_supported; +} js_callback_t; +#endif + /* Synchronet Node Instance class definition */ #if defined(__cplusplus) && defined(JAVASCRIPT) @@ -1305,6 +1381,8 @@ extern "C" { DLLEXPORT void DLLCALL js_EvalOnExit(JSContext*, JSObject*, js_callback_t*); DLLEXPORT void DLLCALL js_PrepareToExecute(JSContext*, JSObject*, const char *filename, const char* startup_dir, JSObject *); DLLEXPORT char* DLLCALL js_getstring(JSContext *cx, JSString *str); + DLLEXPORT JSBool js_handle_events(JSContext *cx, js_callback_t *cb, volatile int *terminated); + DLLEXPORT JSBool js_clear_event(JSContext *cx, uintN argc, jsval *arglist, enum js_event_type et); /* js_system.c */ DLLEXPORT JSObject* DLLCALL js_CreateSystemObject(JSContext* cx, JSObject* parent @@ -1379,6 +1457,8 @@ extern "C" { /* js_console.cpp */ JSObject* js_CreateConsoleObject(JSContext* cx, JSObject* parent); + DLLEXPORT size_t js_cx_input_pending(JSContext *cx); + DLLEXPORT void js_do_lock_input(JSContext *cx, JSBool lock); /* js_bbs.cpp */ JSObject* js_CreateBbsObject(JSContext* cx, JSObject* parent); diff --git a/src/sbbs3/sbbsdefs.h b/src/sbbs3/sbbsdefs.h index cf2d6bf5f9d752d338a463d479e3bd4e7dc9cf17..be1fb144f60b3b429f192ae07b45b7d8ae25ffb3 100644 --- a/src/sbbs3/sbbsdefs.h +++ b/src/sbbs3/sbbsdefs.h @@ -68,20 +68,6 @@ #define JAVASCRIPT_LOAD_PATH_LIST "load_path_list" #define JAVASCRIPT_OPTIONS 0x810 // JSOPTION_JIT | JSOPTION_COMPILE_N_GO -struct js_callback; -typedef struct js_callback { - uint32_t counter; - uint32_t limit; - uint32_t yield_interval; - uint32_t gc_interval; - uint32_t gc_attempts; - uint32_t offline_counter; - BOOL auto_terminate; - volatile BOOL* terminated; - BOOL bg; - struct js_callback *parent_cb; -} js_callback_t; - #define JSVAL_NULL_OR_VOID(val) (JSVAL_IS_NULL(val) || JSVAL_IS_VOID(val)) #ifndef MAX diff --git a/src/sbbs3/services.c b/src/sbbs3/services.c index a27eb58aa8aed63a7201ce502681ecf3c0e142c2..11efc7f59b38a1862f4eb4158edb2d080a1d14cf 100644 --- a/src/sbbs3/services.c +++ b/src/sbbs3/services.c @@ -1147,9 +1147,11 @@ static void js_service_thread(void* arg) if(service->log_level >= LOG_ERR) lprintf(LOG_ERR,"%04d !JavaScript FAILED to compile script (%s)",socket,spath); } else { + service_client.callback.events_supported = TRUE; js_PrepareToExecute(js_cx, js_glob, spath, /* startup_dir */NULL, js_glob); JS_SetOperationCallback(js_cx, js_OperationCallback); JS_ExecuteScript(js_cx, js_glob, js_script, &rval); + js_handle_events(js_cx, &service_client.callback, &terminated); js_EvalOnExit(js_cx, js_glob, &service_client.callback); } JS_RemoveObjectRoot(js_cx, &js_glob); @@ -1259,8 +1261,10 @@ static void js_static_service_thread(void* arg) break; } + service_client.callback.events_supported = TRUE; js_PrepareToExecute(js_cx, js_glob, spath, /* startup_dir */NULL, js_glob); JS_ExecuteScript(js_cx, js_glob, js_script, &rval); + js_handle_events(js_cx, &service_client.callback, &terminated); js_EvalOnExit(js_cx, js_glob, &service_client.callback); JS_RemoveObjectRoot(js_cx, &js_glob); JS_ENDREQUEST(js_cx); diff --git a/src/sbbs3/websrvr.c b/src/sbbs3/websrvr.c index f6edf8df5333b50eb1911ffa511b59837a452f74..bc52448bc9999f601469085e3eb3da3cf98d298c 100644 --- a/src/sbbs3/websrvr.c +++ b/src/sbbs3/websrvr.c @@ -7157,6 +7157,7 @@ void DLLCALL web_server(void* arg) session->js_callback.limit=startup->js.time_limit; session->js_callback.gc_interval=startup->js.gc_interval; session->js_callback.yield_interval=startup->js.yield_interval; + session->js_callback.events_supported = FALSE; pthread_mutex_unlock(&session->struct_filled); session=NULL; served++; diff --git a/src/xpdev/sockwrap.c b/src/xpdev/sockwrap.c index eee5ed44398d83493c5256efd34bf8f70ba601e3..b9c5eb5b40a1449a7fb27e21a4b3a8cf2a9d5b93 100644 --- a/src/xpdev/sockwrap.c +++ b/src/xpdev/sockwrap.c @@ -733,3 +733,81 @@ DLLEXPORT int xp_inet_pton(int af, const char *src, void *dst) freeaddrinfo(res); return 1; } + +#ifdef _WIN32 +DLLEXPORT int +socketpair(int domain, int type, int protocol, SOCKET *sv) +{ + union xp_sockaddr la = {0}; + const int ra = 1; + SOCKET ls; + SOCKET *check; + fd_set rfd; + struct timeval tv; + size_t sa_len; + + sv[0] = sv[1] = INVALID_SOCKET; + ls = socket(domain, type, protocol); + if (ls == INVALID_SOCKET) + goto fail; + switch (domain) { + case PF_INET: + if (inet_ptoaddr("127.0.0.1", &la, sizeof(la)) == NULL) + goto fail; + sa_len = sizeof(la.in); + break; + case PF_INET6: + if (inet_ptoaddr("::1", &la, sizeof(la)) == NULL) + goto fail; + sa_len = sizeof(la.in6); + break; + default: + goto fail; + } + inet_setaddrport(&la, 0); + if (setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, (const char *)&ra, sizeof(ra)) == -1) + goto fail; + if (bind(ls, &la.addr, sa_len) == -1) + goto fail; + if (getsockname(ls, &la.addr, &sa_len) == -1) + goto fail; + if (listen(ls, 1) == -1) + goto fail; + sv[0] = socket(la.addr.sa_family, type, protocol); + if (sv[0] == INVALID_SOCKET) + goto fail; + if (connect(sv[0], &la.addr, sa_len) == -1) + goto fail; + sv[1] = accept(ls, NULL, NULL); + if (sv[1] == INVALID_SOCKET) + goto fail; + closesocket(ls); + ls = INVALID_SOCKET; + + if (send(sv[1], (const char *)&sv, sizeof(sv), 0) != sizeof(sv)) + goto fail; + tv.tv_sec = 0; + tv.tv_usec = 50000; + FD_ZERO(&rfd); + FD_SET(sv[0], &rfd); + if (select(sv[0] + 1, &rfd, NULL, NULL, &tv) != 1) + goto fail; + if (recv(sv[0], (char *)&check, sizeof(check), 0) != sizeof(check)) + goto fail; + if (check != sv) + goto fail; + return 0; + +fail: + if (ls != INVALID_SOCKET) + closesocket(ls); + if (sv[0] != INVALID_SOCKET) + closesocket(sv[0]); + sv[0] = INVALID_SOCKET; + if (sv[1] != INVALID_SOCKET) + closesocket(sv[1]); + sv[1] = INVALID_SOCKET; + return -1; +} +#endif + diff --git a/src/xpdev/sockwrap.h b/src/xpdev/sockwrap.h index 79f394669312d72e57e184a14894df07bfdf4d3f..64481c2f5e74ad28b14d0aac2b222c0231ea0bd6 100644 --- a/src/xpdev/sockwrap.h +++ b/src/xpdev/sockwrap.h @@ -230,6 +230,7 @@ DLLEXPORT void set_socket_errno(int); DLLEXPORT int xp_inet_pton(int af, const char *src, void *dst); #if defined(_WIN32) // mingw and WinXP's WS2_32.DLL don't have inet_pton(): #define inet_pton xp_inet_pton +DLLEXPORT int socketpair(int domain, int type, int protocol, SOCKET *sv); #endif /*