├── rust-toolchain.toml ├── valgrind.sh ├── src ├── modules │ ├── lib │ │ └── mod.rs │ ├── db │ │ └── mod.rs │ ├── com │ │ ├── mod.rs │ │ └── http │ │ │ ├── response.rs │ │ │ ├── request.rs │ │ │ ├── client.rs │ │ │ └── mod.rs │ ├── util │ │ ├── mod.rs │ │ └── cache │ │ │ └── mod.rs │ ├── io │ │ ├── mod.rs │ │ ├── gpio │ │ │ ├── pinset.rs │ │ │ └── mod.rs │ │ └── fs │ │ │ └── mod.rs │ ├── mod.rs │ ├── crypto │ │ └── mod.rs │ ├── parsers │ │ └── mod.rs │ ├── encoding │ │ └── mod.rs │ └── jwt │ │ └── mod.rs ├── features │ ├── mod.rs │ ├── js_fetch │ │ ├── proxies.rs │ │ ├── mod.rs │ │ └── spec.rs │ └── require.rs ├── preprocessors │ ├── macros.rs │ ├── mod.rs │ └── cpp.rs ├── lib.rs └── moduleloaders │ └── mod.rs ├── modules ├── utils │ ├── utils.mes │ └── assertions.mes ├── io │ └── gpio │ │ ├── button.mes │ │ ├── led.mes │ │ ├── stepper.mes │ │ └── servo.mes ├── com │ ├── impls │ │ └── tracrpc.mes │ └── jsonrpc.mes └── dom │ └── htmldom.ts ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── .github └── workflows │ ├── rust_pr.yml │ └── rust.yml ├── Cargo.toml └── README.md /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" -------------------------------------------------------------------------------- /valgrind.sh: -------------------------------------------------------------------------------- 1 | cargo clean 2 | cargo test 3 | find ./target/debug/deps/green_copper_runtime-* -maxdepth 1 -type f -executable | xargs valgrind --leak-check=full --error-exitcode=1 4 | -------------------------------------------------------------------------------- /src/modules/lib/mod.rs: -------------------------------------------------------------------------------- 1 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 2 | 3 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 4 | // todo 5 | builder 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/db/mod.rs: -------------------------------------------------------------------------------- 1 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 2 | 3 | #[cfg(feature = "sqlx")] 4 | pub mod sqlx; 5 | 6 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 7 | #[cfg(feature = "sqlx")] 8 | let builder = sqlx::init(builder); 9 | builder 10 | } 11 | -------------------------------------------------------------------------------- /modules/utils/utils.mes: -------------------------------------------------------------------------------- 1 | import {Assertions as assert} from 'https://raw.githubusercontent.com/HiRoFa/GreenCopperRuntime/main/modules/utils/assertions.mes'; 2 | 3 | export class Utils { 4 | 5 | static "with"(obj) { 6 | // todo 7 | } 8 | 9 | static awaitAll(...args) { 10 | return Promise.all(args.flat()); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/modules/com/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | revisit this when figured out how to add custom client to fetch api like Deno does 4 | 5 | #[cfg(any(feature = "all", feature = "com", feature = "http"))] 6 | pub mod http; 7 | 8 | pub(crate) fn init(builder: &mut B) { 9 | #[cfg(any(feature = "all", feature = "com", feature = "http"))] 10 | http::init(builder); 11 | } 12 | 13 | */ 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .idea 13 | 14 | *.log 15 | test.txt -------------------------------------------------------------------------------- /src/modules/util/mod.rs: -------------------------------------------------------------------------------- 1 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 2 | 3 | #[cfg(any(feature = "all", feature = "util", feature = "cache"))] 4 | pub mod cache; 5 | 6 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 7 | // todo 8 | #[cfg(any(feature = "all", feature = "util", feature = "cache"))] 9 | let builder = cache::init(builder); 10 | 11 | builder 12 | } 13 | -------------------------------------------------------------------------------- /modules/io/gpio/button.mes: -------------------------------------------------------------------------------- 1 | import {Assertions as assert} from 'https://raw.githubusercontent.com/HiRoFa/GreenCopperRuntime/main/modules/utils/assertions.mes'; 2 | import {PinSet} from 'greco://gpio'; 3 | 4 | export class Button { 5 | constructor(pinSet) { 6 | this.pinSet = pinSet; 7 | } 8 | 9 | /** 10 | * init a new Button 11 | */ 12 | static async init(chip = '/dev/gpiochip0', pinNum) { 13 | throw Error("NYI"); 14 | } 15 | } -------------------------------------------------------------------------------- /src/features/mod.rs: -------------------------------------------------------------------------------- 1 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 2 | 3 | #[cfg(feature = "fetch")] 4 | pub mod js_fetch; 5 | #[cfg(feature = "commonjs")] 6 | pub mod require; 7 | 8 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 9 | #[cfg(feature = "commonjs")] 10 | let builder = require::init(builder); 11 | 12 | #[cfg(feature = "http")] 13 | let builder = js_fetch::init(builder); 14 | 15 | builder 16 | } 17 | -------------------------------------------------------------------------------- /src/preprocessors/macros.rs: -------------------------------------------------------------------------------- 1 | use quickjs_runtime::jsutils::{JsError, Script, ScriptPreProcessor}; 2 | 3 | pub struct MacrosPreProcessor {} 4 | 5 | impl MacrosPreProcessor { 6 | pub fn new() -> Self { 7 | Self {} 8 | } 9 | } 10 | 11 | impl ScriptPreProcessor for MacrosPreProcessor { 12 | fn process(&self, _script: &mut Script) -> Result<(), JsError> { 13 | Ok(()) 14 | } 15 | } 16 | 17 | impl Default for MacrosPreProcessor { 18 | fn default() -> Self { 19 | Self::new() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/preprocessors/mod.rs: -------------------------------------------------------------------------------- 1 | //! IfDefPreProcessor 2 | //! 3 | //! this is a script preprocessor which can be used to conditionally load/unload pieces of script before compilation or evaluation 4 | //! 5 | //! # Example 6 | //! ```javascript 7 | //! 8 | //! ``` 9 | 10 | use crate::preprocessors::cpp::CppPreProcessor; 11 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 12 | 13 | pub mod cpp; 14 | pub mod macros; 15 | 16 | pub fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 17 | builder.script_pre_processor(CppPreProcessor::new()) 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/io/mod.rs: -------------------------------------------------------------------------------- 1 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 2 | 3 | #[cfg(any(feature = "all", feature = "io", feature = "gpio"))] 4 | pub mod gpio; 5 | 6 | #[cfg(any(feature = "all", feature = "io", feature = "fs"))] 7 | pub mod fs; 8 | 9 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 10 | #[cfg(any(feature = "all", feature = "io", feature = "gpio"))] 11 | let builder = gpio::init(builder); 12 | 13 | #[cfg(any(feature = "all", feature = "io", feature = "fs"))] 14 | let builder = fs::init(builder); 15 | 16 | builder 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.2.1 2 | 3 | * bugfix: error when calling mysqlcon.query from within query consumer 4 | 5 | # 0.2.0 6 | 7 | * sqlx feature for connecting to mysql or postgres with a shared api 8 | 9 | # 0.1.1 10 | 11 | * Element.hasAttributes 12 | * Element.attributes 13 | * qjs_rt 0.11 14 | 15 | # 0.1.0 16 | 17 | * uses quickjs_runtime 0.10.0 and ditches the multi engine principles 18 | * removed console in favour of impl in quickjs_runtime 19 | * added opt features for console/settimeout/setinterval/setimmediate 20 | * added support for dataset/style objs on Elements in htmldom 21 | 22 | # 0.0.4 23 | 24 | * first JWT impl (greco://jwt) 25 | 26 | # 0.0.2 (work in progress) 27 | 28 | ## 28 feb 2022 29 | 30 | * added crypto.randomUUID() method 31 | 32 | ## older 33 | 34 | * ifdef preprocessor, uses [gpp](https://github.com/Kestrer/gpp) 35 | * added abstract console 36 | * removed old fetchresponseprovider and added new Fetch api (wip) 37 | * added greco://cache as a simple caching module 38 | 39 | # 0.0.1 40 | 41 | * started tracking changes -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 HiRoFa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 2 | 3 | pub mod com; 4 | #[cfg(any(feature = "all", feature = "crypto"))] 5 | pub mod crypto; 6 | pub mod db; 7 | pub mod io; 8 | #[cfg(any(feature = "all", feature = "jwt"))] 9 | pub mod jwt; 10 | pub mod lib; 11 | #[cfg(any(feature = "all", feature = "parsers"))] 12 | pub mod parsers; 13 | pub mod util; 14 | 15 | #[cfg(any(feature = "all", feature = "encoding"))] 16 | pub mod encoding; 17 | 18 | #[cfg(any(feature = "all", feature = "htmldom"))] 19 | pub mod htmldom; 20 | 21 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 22 | //com::init(builder); 23 | let builder = db::init(builder); 24 | let builder = io::init(builder); 25 | let builder = lib::init(builder); 26 | #[cfg(any(feature = "all", feature = "crypto"))] 27 | let builder = crypto::init(builder); 28 | #[cfg(any(feature = "all", feature = "jwt"))] 29 | let builder = jwt::init(builder); 30 | #[cfg(any(feature = "all", feature = "htmldom"))] 31 | let builder = htmldom::init(builder); 32 | #[cfg(any(feature = "all", feature = "parsers"))] 33 | let builder = parsers::init(builder); 34 | #[cfg(any(feature = "all", feature = "encoding"))] 35 | let builder = encoding::init(builder); 36 | util::init(builder) 37 | } 38 | -------------------------------------------------------------------------------- /modules/io/gpio/led.mes: -------------------------------------------------------------------------------- 1 | import {Assertions as assert} from 'https://raw.githubusercontent.com/HiRoFa/GreenCopperRuntime/main/modules/utils/assertions.mes'; 2 | import {PinSet} from 'greco://gpio'; 3 | 4 | export class Led { 5 | constructor(pinSet) { 6 | this.pinSet = pinSet; 7 | } 8 | 9 | /** 10 | * init a new Led 11 | */ 12 | static async init(chip, pinNum) { 13 | 14 | assert.is_string(chip, "chip should be a string like '/dev/gpiochip0'"); 15 | assert.is_number(pinNum, "pinNum should be a number"); 16 | 17 | let pinSet = new PinSet(); 18 | let instance = new this(pinSet); 19 | return instance.pinSet.init(chip, 'out', [pinNum]) 20 | .then(() => { 21 | return instance; 22 | }); 23 | } 24 | 25 | /** 26 | * turn the led on 27 | * @returns Promise 28 | */ 29 | on() { 30 | return this.pinSet.setState([1]); 31 | } 32 | 33 | /** 34 | * turn the led off 35 | * @returns Promise 36 | */ 37 | off() { 38 | return this.pinSet.setState([0]); 39 | } 40 | 41 | /** 42 | * turn the led on 43 | * @returns Promise 44 | */ 45 | async is_on() { 46 | return (await this.pinSet.getState(0)) === 1; 47 | } 48 | 49 | /** 50 | * blink the led for a number of seconds 51 | */ 52 | blink(seconds = 5) { 53 | assert.is_number(seconds, "seconds should be a number"); 54 | return this.pinSet.sequence([[1], [0], [0], [0]], 250, seconds); 55 | } 56 | } -------------------------------------------------------------------------------- /.github/workflows/rust_pr.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUSTFLAGS: -D warnings 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Prepare 17 | run: | 18 | # sudo apt update 19 | sudo apt install gcc-7 ccache llvm autoconf2.13 automake clang python valgrind -y 20 | - name: Cache cargo registry 21 | uses: actions/cache@v2 22 | with: 23 | path: ~/.cargo/registry 24 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }} 25 | - name: Cache cargo index 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.cargo/git 29 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.toml') }} 30 | - name: Ccache 31 | uses: actions/cache@v2 32 | with: 33 | path: ~/.ccache 34 | key: ${{ runner.OS }}-ccache-${{ hashFiles('**\Cargo.toml') }} 35 | - name: Build 36 | run: | 37 | export SHELL=/bin/bash 38 | export CC=/usr/bin/clang 39 | export CXX=/usr/bin/clang++ 40 | ccache -z 41 | CCACHE=$(which ccache) cargo build --verbose 42 | ccache -s 43 | cargo clean 44 | - name: Run tests 45 | run: cargo test --verbose 46 | - name: Clippy check 47 | uses: actions-rs/clippy-check@v1 48 | with: 49 | token: ${{ secrets.GITHUB_TOKEN }} 50 | -------------------------------------------------------------------------------- /src/modules/crypto/mod.rs: -------------------------------------------------------------------------------- 1 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 2 | use quickjs_runtime::jsutils::jsproxies::JsProxy; 3 | use quickjs_runtime::jsutils::JsError; 4 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 5 | 6 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 7 | builder.realm_adapter_init_hook(|_rt, realm| { 8 | // init crypto proxy 9 | init_crypto_proxy(realm) 10 | }) 11 | } 12 | 13 | // todo simple hashes 14 | // https://www.geeksforgeeks.org/node-js-crypto-createhash-method/?ref=lbp 15 | 16 | fn init_crypto_proxy(realm: &QuickJsRealmAdapter) -> Result<(), JsError> { 17 | let crypto_proxy = JsProxy::new().name("crypto").static_method( 18 | "randomUUID", 19 | |_rt, realm: &QuickJsRealmAdapter, _args| { 20 | let uuid = uuid::Uuid::new_v4().to_string(); 21 | realm.create_string(uuid.as_str()) 22 | }, 23 | ); 24 | realm.install_proxy(crypto_proxy, true)?; 25 | Ok(()) 26 | } 27 | 28 | #[cfg(test)] 29 | pub mod tests { 30 | use crate::init_greco_rt; 31 | use futures::executor::block_on; 32 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 33 | use quickjs_runtime::jsutils::Script; 34 | 35 | #[test] 36 | fn test_uuid() { 37 | let rt = init_greco_rt(QuickJsRuntimeBuilder::new()).build(); 38 | let script = Script::new("uuid.js", "crypto.randomUUID();"); 39 | #[allow(clippy::ok_expect)] 40 | let res = block_on(rt.eval(None, script)).ok().expect("script failed"); 41 | assert!(res.is_string()); 42 | //println!("uuid={}", res.get_str()); 43 | assert_eq!(res.get_str().len(), 36); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /modules/com/impls/tracrpc.mes: -------------------------------------------------------------------------------- 1 | import {Client as JsonRpcClient} from 'https://raw.githubusercontent.com/HiRoFa/GreenCopperRuntime/main/modules/com/jsonrpc.mes'; 2 | import {Assertions as assert} from 'https://raw.githubusercontent.com/HiRoFa/GreenCopperRuntime/main/modules/utils/assertions.mes'; 3 | 4 | export class Client { 5 | constructor(url, user, pass){ 6 | 7 | this._jsonRpcClient = new JsonRpcClient() 8 | .setUrl(url) 9 | .setCredentials(user, pass); 10 | 11 | } 12 | 13 | call(method, params) { 14 | return this._jsonRpcClient.call(method, params); 15 | } 16 | 17 | /** 18 | * query tickets based on a filter object 19 | * @example 20 | * let ticketsProm = tracClient.queryTickets({milestone: "4.0.10", owner: "Harry"}); 21 | * @returns Promise> 22 | **/ 23 | queryTickets(filter, max = 100, page = 1) { 24 | 25 | assert.is_object(filter, "filter should be an object"); 26 | assert.is_number(max, "max should be a number"); 27 | assert.is_number(page, "page should be a number"); 28 | 29 | filter = {...filter, max, page}; 30 | 31 | let qstr = []; 32 | for (let id in filter) { 33 | qstr.push(id + "=" + encodeURIComponent(filter[id])); 34 | } 35 | return this.call("ticket.query", [qstr.join("&")]); 36 | } 37 | 38 | /** 39 | * get a Ticket based on an ID 40 | * @returns Promise 41 | **/ 42 | getTicket(id) { 43 | 44 | assert.is_number(id, "id should be a number"); 45 | 46 | return this.call("ticket.get", [id]).then((result) => { 47 | return new Ticket(result); 48 | }); 49 | 50 | } 51 | }; 52 | 53 | export class Ticket { 54 | constructor(data){ 55 | // data = [id, time_created, time_changed, attributes] 56 | this._data = data; 57 | } 58 | 59 | get summary() { 60 | return this._data[3]["summary"]; 61 | } 62 | 63 | get id() { 64 | return this._data[0]; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /modules/dom/htmldom.ts: -------------------------------------------------------------------------------- 1 | import * as grecoDom from 'greco://htmldom'; 2 | 3 | export type Node = { 4 | nodeValue: string, 5 | textContent: string, 6 | parentElement: null | Element, 7 | insertBefore: (node: Node, referenceNode: Node) => Node, 8 | insertAfter: (node: Node, referenceNode: Node) => Node, 9 | nodeType: number, 10 | ownerDocument: Document 11 | }; 12 | 13 | export type NodeList = Iterable & { 14 | length: number, 15 | item: (index: number) => Node, 16 | forEach: (callbackFn: (element: Node, index: number, list: NodeList) => void, thisArg: any) => void 17 | }; 18 | 19 | export type ElementList = Iterable & { 20 | length: number, 21 | item: (index: number) => Element, 22 | forEach: (callbackFn: (element: Element, index: number, list: ElementList) => void, thisArg: any) => void 23 | }; 24 | 25 | export type TextNode = Node & { 26 | 27 | }; 28 | 29 | export type Element = Node & { 30 | children: ElementList, 31 | childNodes: NodeList, 32 | 33 | firstChild: Node, 34 | lastChild: Node, 35 | 36 | previousSibling: null | Node, 37 | nextSibling: null | Node, 38 | nextElementSibling: null | Element, 39 | previousElementSibling: null | Element, 40 | 41 | getAttribute: (name: string) => null | string, 42 | setAttribute: (name: string, value: null | string) => void, 43 | 44 | innerHTML: string, 45 | outerHTML: string, 46 | 47 | className: null | string, 48 | localName: string, 49 | tagName: string, 50 | 51 | querySelector: (selectors: string) => null | Element, 52 | querySelectorAll: (selectors: string) => ElementList, 53 | 54 | appendChild: (node: Node) => Node, 55 | append: (...child: Array) => void, 56 | removeChild: (node: Node) => Node, 57 | replaceChild: (newChild: Node, oldChild: Node) => Node 58 | }; 59 | 60 | export type Document = Element & { 61 | body: Element, 62 | documentElement: Element, 63 | createElement: (localName: string) => Element, 64 | createTextNode: (data: string) => TextNode, 65 | getElementById: (id: string) => Element 66 | }; 67 | 68 | export type GrecoDOMParser = { 69 | parseFromString: (html: string | Uint8Array, mimeType: string) => Document, 70 | parseFromStringAsync: (html: string | Uint8Array, mimeType: string) => Promise; 71 | }; 72 | 73 | export const DOMParser: GrecoDOMParser = grecoDom.DOMParser; -------------------------------------------------------------------------------- /modules/utils/assertions.mes: -------------------------------------------------------------------------------- 1 | export class Assertions { 2 | 3 | static is_true(val, msg) { 4 | if (typeof msg !== "string") { 5 | throw Error("no msg passed to assertion method"); 6 | } 7 | if (val !== true) { 8 | throw Error("assertion failed: " + msg); 9 | } 10 | } 11 | 12 | static is_equal(valA, valB, msg) { 13 | this.is_true(valA === valB, msg); 14 | } 15 | 16 | static is_lt(valA, valB, msg) { 17 | this.is_true(valA < valB, msg); 18 | } 19 | 20 | static is_gt(valA, valB, msg) { 21 | this.is_true(valA > valB, msg); 22 | } 23 | 24 | static is_false(val, msg) { 25 | this.is_true(val === false, msg); 26 | } 27 | 28 | static not_null(obj, msg) { 29 | this.is_true(undefined !== obj && null !== obj, msg); 30 | } 31 | 32 | static member_is_undefined(obj, memberName, msg) { 33 | this.is_true(typeof obj[memberName] === "undefined", msg) 34 | } 35 | 36 | static member_is_null(obj, memberName, msg) { 37 | this.is_true(null === obj[memberName], msg); 38 | } 39 | 40 | static is_array(obj, msg) { 41 | this.is_true(Array.isArray(obj), msg); 42 | } 43 | 44 | static is_function(obj, msg) { 45 | this.is_true(typeof obj === "function", msg); 46 | } 47 | 48 | static is_object(obj, msg) { 49 | this.is_true(typeof obj === "object", msg); 50 | } 51 | 52 | static is_string(obj, msg) { 53 | this.is_true(typeof obj === "string", msg); 54 | } 55 | 56 | static is_number(obj, msg) { 57 | this.is_true(typeof obj === "number", msg); 58 | } 59 | 60 | static is_boolean(obj, msg) { 61 | this.is_true(typeof obj === "boolean", msg); 62 | } 63 | 64 | static is_instance_of(obj, obj_type, msg) { 65 | this.not_null(obj, msg); 66 | this.not_null(obj_type, "obj_type may not be null when calling Assertions.is_instance_of()."); 67 | let ok = false; 68 | if (typeof obj_type === "string") { 69 | if (typeof obj === obj_type) { 70 | ok = true; 71 | } else if (obj.constructor && obj.constructor.name === obj_type) { 72 | ok = true; 73 | } 74 | } else { 75 | ok = obj instanceof obj_type; 76 | } 77 | this.is_true(ok, msg); 78 | } 79 | 80 | static is_array_of(obj, obj_type, msg) { 81 | this.is_array(obj, msg); 82 | for (let val of obj) { 83 | this.is_instance_of(val, obj_type, msg); 84 | } 85 | } 86 | 87 | }; 88 | -------------------------------------------------------------------------------- /modules/com/jsonrpc.mes: -------------------------------------------------------------------------------- 1 | import {Assertions as assert} from 'https://raw.githubusercontent.com/HiRoFa/GreenCopperRuntime/main/modules/utils/assertions.mes'; 2 | import {HttpClient} from 'greco://http'; 3 | 4 | export class Client { 5 | constructor(version = "1.0"){ 6 | this._httpClient = new HttpClient(); 7 | this._version = version; 8 | } 9 | static nextId() { 10 | this.id = (this.id || this.id = 1) + 1; 11 | return this.id; 12 | } 13 | call(method, params) { 14 | 15 | assert.is_string(this._url, "no url set for this JsonRpcClient"); 16 | assert.is_string(method, "no method set for this call"); 17 | 18 | let id = Client.next_id(); 19 | 20 | let payload; 21 | // see https://en.wikipedia.org/wiki/JSON-RPC 22 | switch (this._version) { 23 | case "1.1": 24 | assert.is_array(params, "no properties set for this call or not an array"); 25 | payload = {version: "1.1", method, params, id}; 26 | break; 27 | case "2.0": 28 | assert.is_object(params, "no properties set for this call or not an object"); 29 | payload = {version: "2.0", method, params, id}; 30 | break; 31 | default: // 1.0 32 | assert.is_array(params, "no properties set for this call or not an array"); 33 | payload = {method, params, id}; 34 | break; 35 | } 36 | 37 | let req = this._httpClient.request("POST", this._url); 38 | req.setHeader('Content-Type', 'application/json'); 39 | 40 | console.debug("JSON_RPC: payload: %s", JSON.stringify(payload)); 41 | 42 | return req.send(payload).then((response) => { 43 | return response.json(); 44 | }).then((jsonRes) => { 45 | console.debug("JSON-RPC: jsonRes = %s", JSON.stringify(jsonRes)); 46 | 47 | // err looks like this 48 | // {"error": {"message": "XML_RPC privileges are required to perform this operation. You don't have the required permissions.", "code": 403, "name": "JSONRPCError"}, "result": null, "id": 1} 49 | 50 | if (jsonRes.error) { 51 | throw Error("["+jsonRes.error.name+" : code:"+jsonRes.error.code+"] " + jsonRes.error.message); 52 | } else { 53 | return jsonRes.result; 54 | } 55 | 56 | }); 57 | 58 | } 59 | setCredentials(user, pass) { 60 | assert.is_string(user, "user needs to be a String"); 61 | assert.is_string(pass, "pass needs to be a String"); 62 | this._httpClient.basicAuth(user, pass); 63 | return this; 64 | } 65 | setUrl(url) { 66 | assert.is_string(url, "url needs to be a String"); 67 | this._url = url; 68 | return this; 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | RUSTFLAGS: -D warnings 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Prepare 17 | run: | 18 | # sudo apt update 19 | sudo apt install ccache llvm autoconf2.13 automake clang python2 valgrind -y 20 | - name: Cache cargo registry 21 | uses: actions/cache@v2 22 | with: 23 | path: ~/.cargo/registry 24 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }} 25 | - name: Cache cargo index 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.cargo/git 29 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.toml') }} 30 | - name: Ccache 31 | uses: actions/cache@v2 32 | with: 33 | path: ~/.ccache 34 | key: ${{ runner.OS }}-ccache-${{ hashFiles('**\Cargo.toml') }} 35 | - name: Build 36 | run: | 37 | export SHELL=/bin/bash 38 | export CC=/usr/bin/clang 39 | export CXX=/usr/bin/clang++ 40 | ccache -z 41 | CCACHE=$(which ccache) cargo build --verbose 42 | ccache -s 43 | cargo clean 44 | - name: Format 45 | run: | 46 | cargo fmt 47 | - name: Commit fmt files 48 | run: | 49 | git config --local user.email "action@github.com" 50 | git config --local user.name "GitHub Action" 51 | git commit -m "autofmt" -a || true 52 | git push "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" HEAD:${{ github.ref }} || true 53 | - name: Doc 54 | run: | 55 | cargo doc 56 | - name: Commit docs 57 | run: | 58 | cp -r ./target/doc /tmp 59 | cd /tmp/doc 60 | git init 61 | echo 'Redirect

Redirecting

' >> index.html 62 | git add . 63 | git remote add origin https://github.com/${{ github.repository }}.git 64 | git config --local user.email "action@github.com" 65 | git config --local user.name "GitHub Action" 66 | git commit -m "doc" -a || true 67 | git push "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" HEAD:gh-pages --force || true 68 | - name: Deploy to gh-pages 69 | run: | 70 | curl -X POST https://api.github.com/repos/${{ github.repository }}/pages/builds -H "Accept: application/vnd.github.mister-fantastic-preview+json" -u ${{ github.actor }}:${{ secrets.GH_TOKEN }} 71 | - name: Run tests 72 | run: cargo test --verbose 73 | - name: Clippy check 74 | uses: actions-rs/clippy-check@v1 75 | with: 76 | token: ${{ secrets.GITHUB_TOKEN }} 77 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "green_copper_runtime" 3 | version = "0.2.2" 4 | authors = ["info@hirofa.com"] 5 | edition = "2018" 6 | license = "MIT" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [features] 11 | 12 | default = ["all"] 13 | 14 | crypto = ["uuid"] 15 | jwt = ["jwt-simple", "uuid", "serde", "serde_json"] 16 | 17 | all = ["io", "db", "com", "features", "util", "crypto", "jwt", "htmldom", "parsers", "encoding"] 18 | 19 | encoding = ["base64"] 20 | parsers = ["csvparser"] 21 | csvparser = ["csv"] 22 | htmldom = ["kuchiki", "html5ever"] 23 | 24 | io = ["gpio", "fs"] 25 | db = ["sqlx"] 26 | 27 | fs = [] 28 | gpio = ["gpio-cdev"] 29 | sqlx = ["sqlx_lib"] 30 | 31 | com = ["http"] 32 | http = ["reqwest"] 33 | 34 | features = ["commonjs", "console", "fetch", "settimeout", "setinterval", "setimmediate"] 35 | 36 | commonjs = [] 37 | fetch = ["http"] 38 | console = ["quickjs_runtime/console"] 39 | settimeout = ["quickjs_runtime/settimeout"] 40 | setinterval = ["quickjs_runtime/setinterval"] 41 | setimmediate = ["quickjs_runtime/setimmediate"] 42 | 43 | 44 | util = ["cache"] 45 | cache = ["lru"] 46 | 47 | 48 | 49 | [dependencies] 50 | quickjs_runtime = { version = "0.17" } 51 | #quickjs_runtime = { path = '../quickjs_es_runtime', features = ["typescript", "default"]} 52 | #quickjs_runtime = { git = 'https://github.com/HiRoFa/quickjs_es_runtime', features = ["typescript", "default"]} 53 | #libquickjs-sys = {package="hirofa-quickjs-sys", path='../quickjs-sys', features=["quickjs-ng"]} 54 | #libquickjs-sys = {package="hirofa-quickjs-sys", git='https://github.com/HiRoFa/quickjs-sys', features=["bellard"]} 55 | libquickjs-sys = { package = "hirofa-quickjs-sys", version = "0.12", features = ["bellard"], default-features = false } 56 | hirofa_utils = "0.7" 57 | #hirofa_utils = {git = "https://github.com/HiRoFa/utils"} 58 | #hirofa_utils = { path = '../utils'} 59 | 60 | lazy_static = "1.4.0" 61 | log = "0.4.8" 62 | simple-logging = "2.0.2" 63 | backtrace = "0.3.56" 64 | url = "2.2.1" 65 | gpp = "0.6" 66 | either = "1" 67 | 68 | reqwest = { version = "0.12", features = ["rustls-tls", "cookies", "gzip", "deflate", "multipart", "blocking"], optional = true, default-features = false } 69 | gpio-cdev = { git = "https://github.com/rust-embedded/gpio-cdev", optional = true, features = ["async-tokio", "futures"] } 70 | futures = { version = "0.3" } 71 | sqlx_lib = { package = "sqlx", version = "0.8.6", features = ["mysql", "postgres", "runtime-tokio", "time", "chrono", "uuid", "rust_decimal"], optional = true } 72 | lru = { version = "0.14", optional = true } 73 | csv = { version = "1.1.6", optional = true } 74 | uuid = { version = "1", features = ["v4", "serde"], optional = true } 75 | jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"], optional = true } 76 | serde = { version = "1.0", optional = true } 77 | serde_json = { version = "1.0", optional = true } 78 | num-traits = "0.2" 79 | cached = "0.55" 80 | 81 | regex = "1.8.1" 82 | 83 | #kuchiki = {version="0.8.1", optional = true} 84 | kuchiki = { package = "kuchikiki", git = "https://github.com/HiRoFa/kuchikiki", optional = true } 85 | html5ever = { version = "0.27", optional = true } 86 | base64 = { version = "0.22", optional = true } 87 | #kuchiki = {git="https://github.com/kuchiki-rs/kuchiki#f92e4c047fdc30619555da282ac7ccce1d313aa6", optional = true} 88 | #html5ever = {version="0.26", optional = true} 89 | 90 | tokio = { version = "1", features = ["rt", "macros"] } 91 | anyhow = "1" 92 | 93 | [dev-dependencies] 94 | 95 | simple-logging = "2.0.2" 96 | -------------------------------------------------------------------------------- /src/modules/com/http/response.rs: -------------------------------------------------------------------------------- 1 | use hirofa_utils::js_utils::JsError; 2 | use quickjs_runtime::quickjs_utils::{json, primitives}; 3 | use quickjs_runtime::quickjscontext::QuickJsContext; 4 | use quickjs_runtime::reflection; 5 | use quickjs_runtime::reflection::Proxy; 6 | use quickjs_runtime::valueref::JSValueRef; 7 | use std::cell::RefCell; 8 | use std::collections::HashMap; 9 | use std::mem::replace; 10 | 11 | pub(crate) struct UreqResponseWrapper { 12 | pub(crate) delegate: Option, 13 | } 14 | 15 | type HttpResponseType = UreqResponseWrapper; 16 | 17 | thread_local! { 18 | static HTTP_RESPONSE_INSTANCES: RefCell> = 19 | RefCell::new(HashMap::new()); 20 | } 21 | 22 | fn with_http_response(instance_id: &usize, consumer: C) -> R 23 | where 24 | C: Fn(&mut HttpResponseType) -> R, 25 | { 26 | HTTP_RESPONSE_INSTANCES.with(move |instances_rc| { 27 | let instances = &mut *instances_rc.borrow_mut(); 28 | let i = instances 29 | .get_mut(instance_id) 30 | .expect("not a valid instance id"); 31 | consumer(i) 32 | }) 33 | } 34 | 35 | pub(crate) fn reg_instance( 36 | q_ctx: &QuickJsContext, 37 | response_obj: HttpResponseType, 38 | ) -> Result { 39 | let resp_proxy = reflection::get_proxy(q_ctx, "greco.com.http.Response") 40 | .expect("could not find greco.com.http.Response proxy"); 41 | 42 | let instance_res = reflection::new_instance2(&resp_proxy, q_ctx)?; 43 | 44 | let response_obj_id = instance_res.0; 45 | let response_instance_ref = instance_res.1; 46 | 47 | HTTP_RESPONSE_INSTANCES.with(|requests_rc| { 48 | let requests = &mut *requests_rc.borrow_mut(); 49 | requests.insert(response_obj_id, response_obj); 50 | }); 51 | 52 | Ok(response_instance_ref) 53 | } 54 | 55 | pub(crate) fn init_http_response_proxy( 56 | q_ctx: &QuickJsContext, 57 | namespace: Vec<&'static str>, 58 | ) -> Result { 59 | Proxy::new() 60 | .name("Response") 61 | .namespace(namespace) 62 | .method("json", |q_ctx, obj_id, _args| { 63 | // todo, should return a promise, at which point things will suck because the response in in an thread local in the wrong thread.,.... 64 | // so box in Arc in the hashmap and move a clone of that arc to the producer thread? 65 | let text_content_opt = with_http_response(obj_id, |response_wrapper| { 66 | response_wrapper.delegate.as_ref()?; // this returns None if delegate is none 67 | let ureq_response: ureq::Response = 68 | replace(&mut response_wrapper.delegate, None).unwrap(); 69 | 70 | let res = ureq_response.into_string(); 71 | let text = res.expect("request failed"); 72 | log::trace!("got text from http resp: {}", text); 73 | Some(text) 74 | }); 75 | 76 | if let Some(text_content) = text_content_opt { 77 | json::parse_q(q_ctx, text_content.as_str()) 78 | } else { 79 | Err(JsError::new_str("response was already consumed")) 80 | } 81 | }) 82 | .getter_setter( 83 | "text", 84 | |q_ctx, obj_id| { 85 | let text_content_opt = with_http_response(obj_id, |response_wrapper| { 86 | response_wrapper.delegate.as_ref()?; 87 | let ureq_response: ureq::Response = 88 | replace(&mut response_wrapper.delegate, None).unwrap(); 89 | 90 | let res = ureq_response.into_string(); 91 | let text = res.expect("request failed"); 92 | log::trace!("got text from http resp: {}", text); 93 | Some(text) 94 | }); 95 | 96 | if let Some(text_content) = text_content_opt { 97 | primitives::from_string_q(q_ctx, text_content.as_str()) 98 | } else { 99 | Err(JsError::new_str("response was already consumed")) 100 | } 101 | }, 102 | |_q_js_rt, _obj_id, _v| Ok(()), 103 | ) 104 | .install(q_ctx, false) 105 | } 106 | -------------------------------------------------------------------------------- /src/features/js_fetch/proxies.rs: -------------------------------------------------------------------------------- 1 | use crate::features::js_fetch::spec::Response; 2 | use quickjs_runtime::jsutils::jsproxies::JsProxy; 3 | use quickjs_runtime::jsutils::JsError; 4 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 5 | use std::cell::RefCell; 6 | use std::collections::HashMap; 7 | use std::sync::Arc; 8 | 9 | pub(crate) fn impl_for(realm: &QuickJsRealmAdapter) -> Result<(), JsError> { 10 | impl_response(realm)?; 11 | Ok(()) 12 | } 13 | 14 | thread_local! { 15 | pub(crate) static RESPONSE_INSTANCES: RefCell>> = RefCell::new(HashMap::new()); 16 | } 17 | 18 | fn with_response) -> R, R>(id: &usize, consumer: C) -> Result { 19 | RESPONSE_INSTANCES.with(|rc| { 20 | let map = &*rc.borrow(); 21 | if let Some(response) = map.get(id) { 22 | Ok(consumer(response)) 23 | } else { 24 | Err("instance not found") 25 | } 26 | }) 27 | } 28 | 29 | fn impl_response(realm: &QuickJsRealmAdapter) -> Result<(), JsError> { 30 | let response_proxy = JsProxy::new() 31 | .namespace(&[]) 32 | .name("Response") 33 | .finalizer(|_rt, _realm, id| { 34 | // todo.. need to use realm id as part of key? 35 | RESPONSE_INSTANCES.with(|rc| { 36 | let map = &mut *rc.borrow_mut(); 37 | map.remove(&id); 38 | }); 39 | }) 40 | .getter("ok", |_rt, realm, instance_id| { 41 | with_response(instance_id, |response| { 42 | // 43 | realm.create_boolean(response.ok) 44 | }) 45 | // todo with_response is impld sucky 46 | .unwrap() 47 | }) 48 | .getter("status", |_rt, realm, instance_id| { 49 | with_response(instance_id, |response| { 50 | // 51 | realm.create_i32(response.status as i32) 52 | }) 53 | // todo with_response is impld sucky 54 | .unwrap() 55 | }) 56 | .method("text", |_rt, realm, instance_id, _args| { 57 | // 58 | let response = with_response(instance_id, |response| response.clone()) 59 | .map_err(JsError::new_str)?; 60 | // todo promise may seem futile now but later we will just store bytes in body and encode to string async 61 | realm.create_resolving_promise_async( 62 | async move { response.text().await }, 63 | // todo js_string_crea2 with String 64 | |realm, res| realm.create_string(res.as_str()), 65 | ) 66 | }) 67 | .method("json", |_rt, realm, instance_id, _args| { 68 | // 69 | let response = with_response(instance_id, |response| response.clone()) 70 | .map_err(JsError::new_str)?; 71 | // todo promise may seem futile now but later we will just store bytes in body and encode to string async 72 | realm.create_resolving_promise_async( 73 | async move { response.text().await }, 74 | // todo js_string_crea2 with String 75 | |realm, res| realm.json_parse(res.as_str()), 76 | ) 77 | }) 78 | // non std util method, need to impl readablestream and such later 79 | .method("bytes", |_rt, realm, instance_id, _args| { 80 | // 81 | let response = with_response(instance_id, |response| response.clone()) 82 | .map_err(JsError::new_str)?; 83 | // todo promise may seem futile now but later we will just store bytes in body and encode to string async 84 | realm.create_resolving_promise_async( 85 | async move { response.bytes().await }, 86 | // todo js_string_crea2 with String 87 | |realm, res| realm.create_typed_array_uint8(res), 88 | ) 89 | }) 90 | // non std header, todo create Headers proxy 91 | .method("getHeader", |_rt, realm, instance_id, args| { 92 | // 93 | let response = with_response(instance_id, |response| response.clone()) 94 | .map_err(JsError::new_str)?; 95 | 96 | if args.is_empty() || !args[0].is_string() { 97 | return Err(JsError::new_str("getHeader expects a single String arg")); 98 | } 99 | 100 | let name = args[0].to_string()?; 101 | if let Some(headers) = response.headers.get(name.as_str()) { 102 | if !headers.is_empty() { 103 | return realm.create_string(headers.first().unwrap()); 104 | } 105 | } 106 | 107 | realm.create_null() 108 | }); 109 | 110 | realm.install_proxy(response_proxy, false)?; 111 | 112 | Ok(()) 113 | } 114 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 2 | 3 | #[allow(unused_imports)] 4 | #[macro_use] 5 | extern crate lazy_static; 6 | extern crate core; 7 | 8 | #[cfg(any( 9 | feature = "all", 10 | feature = "features", 11 | feature = "console", 12 | feature = "fetch" 13 | ))] 14 | pub mod features; 15 | 16 | pub mod moduleloaders; 17 | 18 | pub mod modules; 19 | pub mod preprocessors; 20 | 21 | pub fn init_greco_rt(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 22 | init_greco_rt2(builder, true, true, true) 23 | } 24 | 25 | pub fn init_greco_rt2( 26 | builder: QuickJsRuntimeBuilder, 27 | preprocs: bool, 28 | features: bool, 29 | modules: bool, 30 | ) -> QuickJsRuntimeBuilder { 31 | let mut builder = builder; 32 | if modules { 33 | builder = modules::init(builder); 34 | } 35 | if features { 36 | builder = features::init(builder); 37 | } 38 | if preprocs { 39 | builder = preprocessors::init(builder); 40 | } 41 | builder 42 | } 43 | 44 | #[cfg(test)] 45 | pub mod tests { 46 | 47 | use crate::preprocessors::cpp::CppPreProcessor; 48 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 49 | use quickjs_runtime::facades::QuickJsRuntimeFacade; 50 | use quickjs_runtime::values::JsValueFacade; 51 | use std::panic; 52 | 53 | fn init_abstract_inner(rt_facade: &QuickJsRuntimeFacade) { 54 | let res = rt_facade.loop_realm_sync(None, |_rt, ctx_adapter| { 55 | ctx_adapter.install_closure( 56 | &["com", "my_company"], 57 | "testFunction", 58 | |_runtime, realm, _this, args| { 59 | // return 1234 60 | let arg1 = &args[0].to_i32(); 61 | let arg2 = &args[1].to_i32(); 62 | realm.create_i32(arg1 * arg2 * 3) 63 | }, 64 | 2, 65 | ) 66 | }); 67 | match res { 68 | Ok(_) => {} 69 | Err(err) => { 70 | panic!("could not init: {}", err); 71 | } 72 | } 73 | } 74 | 75 | fn test_abstract_inner(rt_facade: &QuickJsRuntimeFacade) { 76 | let args: Vec = vec![JsValueFacade::new_i32(2), JsValueFacade::new_i32(4)]; 77 | let res = 78 | rt_facade.invoke_function_sync(None, &["com", "my_company"], "testFunction", args); 79 | match res { 80 | Ok(val) => { 81 | if let JsValueFacade::I32 { val } = val { 82 | assert_eq!(val, 2 * 4 * 3); 83 | } else { 84 | panic!("script did not return a i32") 85 | } 86 | } 87 | Err(err) => { 88 | panic!("func failed: {}", err); 89 | } 90 | } 91 | } 92 | 93 | #[test] 94 | fn test_abstract() { 95 | /* 96 | panic::set_hook(Box::new(|panic_info| { 97 | let backtrace = Backtrace::new(); 98 | println!("thread panic occurred: {panic_info}\nbacktrace: {backtrace:?}"); 99 | log::error!( 100 | "thread panic occurred: {}\nbacktrace: {:?}", 101 | panic_info, 102 | backtrace 103 | ); 104 | })); 105 | 106 | simple_logging::log_to_file("greco_rt.log", LevelFilter::Trace) 107 | .ok() 108 | .unwrap(); 109 | */ 110 | { 111 | println!("testing quickjs"); 112 | let quickjs_builder = QuickJsRuntimeBuilder::new(); 113 | //let builder1: JsRuntimeBuilder = quickjs_builder; 114 | let rt1 = quickjs_builder.build(); 115 | init_abstract_inner(&rt1); 116 | test_abstract_inner(&rt1); 117 | } 118 | } 119 | 120 | #[test] 121 | fn test1() { 122 | let rt = init_test_greco_rt(); 123 | drop(rt); 124 | } 125 | 126 | pub fn init_test_greco_rt() -> QuickJsRuntimeFacade { 127 | /* 128 | panic::set_hook(Box::new(|panic_info| { 129 | let backtrace = Backtrace::new(); 130 | println!("thread panic occurred: {panic_info}\nbacktrace: {backtrace:?}"); 131 | log::error!( 132 | "thread panic occurred: {}\nbacktrace: {:?}", 133 | panic_info, 134 | backtrace 135 | ); 136 | })); 137 | 138 | simple_logging::log_to_file("greco_rt.log", LevelFilter::Trace) 139 | .ok() 140 | .unwrap(); 141 | */ 142 | let builder = QuickJsRuntimeBuilder::new().script_pre_processor(CppPreProcessor::new()); 143 | 144 | builder.build() 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GreenCopperRuntime 2 | 3 | **Just to get thing clear straight away, this is a very much work in progress project, nothing is definitive, it might never become definitive** 4 | 5 | # Roadmap / The plan 6 | 7 | GreenCopperRuntime is a library which adds additional features to a QuickJs JavaScript runtime. 8 | 9 | GreenCopperRuntime is based on [quickjs_runtime](https://github.com/HiRoFa/quickjs_es_runtime) 10 | 11 | ## Other GreenCopper projects 12 | 13 | [GreenCopperCmd](https://github.com/HiRoFa/GreenCopperCmd) is a commandline utility which you can use to run js/ts files with GreenCopper 14 | 15 | # Default implementations 16 | 17 | GreenCopperRuntime provides implementations for abstract features of the Runtimes like: 18 | * [x] [FileSystemModuleLoader](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/moduleloaders/struct.FileSystemModuleLoader.html) 19 | * [x] [HTTPModuleLoader](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/moduleloaders/struct.HttpModuleLoader.html) 20 | * [x] [HTTPFetch](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/features/js_fetch/index.html) (http capable implementation of fetch api) 21 | 22 | ### Preprocessing 23 | 24 | GreenCopperRuntime provides script pre-processing for: 25 | * [x] cpp style preprocessing (e.g. use #ifdef $GRECO_DEBUG in code) ([DOCS](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/preprocessors/cpp)) 26 | * [ ] macros which generate script before eval 27 | * [x] Typescript support is implemented as a separate optional project [typescript_utils](https://github.com/HiRoFa/typescript_utils) 28 | 29 | The following features are optionally added by specifying them in your Cargo.toml 30 | 31 | * [x] [HTML Dom](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/modules/htmldom/index.html) (Work in progress) 32 | * [ ] crypto 33 | * [x] crypto.randomUUID() 34 | * [ ] crypto.subtle 35 | * [x] JWT (Work in progress) 36 | * [ ] db 37 | * [x] [mysql](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/modules/db/mysql) (Work in progress) 38 | * [x] single query (named and positional params) 39 | * [x] execute (batch) 40 | * [x] transactions 41 | * [ ] cassandra 42 | * [ ] redis 43 | * [ ] com 44 | * [ ] [http](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/modules/com/http) (Work in progress, was deleted due to fetch being done first. will review this func later for advanced things like client certs) 45 | * [ ] sockets 46 | * [ ] io 47 | * [x] [gpio](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/modules/io/gpio) (Work in progress) 48 | * [x] [fs](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/modules/io/fs) (Work in progress) 49 | * [ ] libloading 50 | * [ ] libc 51 | * [ ] java 52 | * [ ] npm 53 | * [x] [commonjs](https://hirofa.github.io/GreenCopperRuntime/green_copper_runtime/features/require) 54 | * [ ] utilities 55 | * [ ] caching 56 | * [x] cache (WiP) 57 | 58 | # Getting started 59 | 60 | // wip 61 | 62 | ## Cargo.toml 63 | 64 | In your cargo.toml you can add the green_copper dependency and specify the runtimes you want to use (as features) 65 | 66 | ```toml 67 | green_copper_runtime = { git = 'https://github.com/HiRoFa/GreenCopperRuntime', branch="main", features = ["engine_quickjs"]} 68 | quickjs_runtime = {git = 'https://github.com/HiRoFa/quickjs_es_runtime', branch="main"} 69 | ``` 70 | 71 | ## Main api concepts 72 | 73 | // wip 74 | 75 | GreenCopper based runtimes all split the API into two distinct halves, first of all there are your outer thread-safe API's which do not directly call the underlying runtime, These are the 76 | * [QuickJsRuntimeFacade](https://hirofa.github.io/GreenCopperRuntime/hirofa_utils/js_utils/facades/trait.JsRuntimeFacade.html) (represents a Runtime) 77 | * [JsValueFacade](https://hirofa.github.io/GreenCopperRuntime/hirofa_utils/js_utils/facades/values/enum.JsValueFacade.html) (represents a Value) 78 | 79 | All of these work (with some exeptions) by adding a job to an EventLoop (a member of the JsRuntimeFacade) and getting the result async (the API returns a Future). 80 | 81 | These jobs run in a single thread per runtime and provide access to the Adapters which DO interact with the actual Runtime/Context/Value directly, these are: 82 | * [QuickJsRuntimeAdapter](https://hirofa.github.io/GreenCopperRuntime/hirofa_utils/js_utils/adapters/trait.JsRuntimeAdapter.html) (represents a Runtime) 83 | * [QuickJsRealmAdapter](https://hirofa.github.io/GreenCopperRuntime/hirofa_utils/js_utils/adapters/trait.JsRealmAdapter.html) (represents a Context or Realm) 84 | * [QuickJsValueAdapter](https://hirofa.github.io/GreenCopperRuntime/hirofa_utils/js_utils/adapters/trait.JsValueAdapter.html) (represents a Value) 85 | 86 | ## Example 87 | 88 | // todo 89 | 90 | ## Adding features 91 | 92 | // wip 93 | 94 | ### Functions 95 | 96 | // wip 97 | 98 | ### Proxy classes 99 | 100 | // wip 101 | 102 | ### Modules 103 | 104 | // wip -------------------------------------------------------------------------------- /src/modules/com/http/request.rs: -------------------------------------------------------------------------------- 1 | use crate::modules::com::http::response; 2 | use crate::modules::com::http::response::UreqResponseWrapper; 3 | use hirofa_utils::js_utils::JsError; 4 | use log::trace; 5 | use quickjs_runtime::esruntime_utils::promises; 6 | use quickjs_runtime::quickjs_utils::{functions, json, primitives}; 7 | use quickjs_runtime::quickjscontext::QuickJsContext; 8 | use quickjs_runtime::reflection::Proxy; 9 | use quickjs_runtime::valueref::JSValueRef; 10 | use quickjs_runtime::{quickjs_utils, reflection}; 11 | use std::cell::RefCell; 12 | use std::collections::HashMap; 13 | 14 | type HttpRequestType = ureq::Request; 15 | 16 | thread_local! { 17 | static HTTP_REQUEST_INSTANCES: RefCell> = 18 | RefCell::new(HashMap::new()); 19 | } 20 | 21 | pub(crate) fn with_http_request(instance_id: &usize, consumer: C) -> R 22 | where 23 | C: Fn(&mut HttpRequestType) -> R, 24 | { 25 | HTTP_REQUEST_INSTANCES.with(move |instances_rc| { 26 | let instances = &mut *instances_rc.borrow_mut(); 27 | let i = instances 28 | .get_mut(instance_id) 29 | .expect("not a valid instance id"); 30 | consumer(i) 31 | }) 32 | } 33 | 34 | pub(crate) fn reg_instance( 35 | q_ctx: &QuickJsContext, 36 | request_obj: HttpRequestType, 37 | ) -> Result { 38 | let req_proxy = reflection::get_proxy(q_ctx, "greco.com.http.Request") 39 | .expect("could not find greco.com.http.Request proxy"); 40 | 41 | let instance_res = reflection::new_instance2(&req_proxy, q_ctx)?; 42 | 43 | let request_obj_id = instance_res.0; 44 | let request_instance_ref = instance_res.1; 45 | 46 | HTTP_REQUEST_INSTANCES.with(|requests_rc| { 47 | let requests = &mut *requests_rc.borrow_mut(); 48 | requests.insert(request_obj_id, request_obj); 49 | }); 50 | 51 | Ok(request_instance_ref) 52 | } 53 | 54 | pub(crate) fn init_http_request_proxy( 55 | q_ctx: &QuickJsContext, 56 | namespace: Vec<&'static str>, 57 | ) -> Result { 58 | Proxy::new() 59 | .name("Request") 60 | .namespace(namespace) 61 | .method("setHeader", |q_ctx, obj_id, args| { 62 | if args.len() != 2 { 63 | return Err(JsError::new_str("setHeader requires two string arguments")); 64 | } 65 | let name_arg = &args[0]; 66 | let value_arg = &args[1]; 67 | 68 | if !value_arg.is_string() || !name_arg.is_string() { 69 | return Err(JsError::new_str("setHeader requires two string arguments")); 70 | } 71 | 72 | let name_str = primitives::to_string_q(q_ctx, name_arg)?; 73 | let value_str = primitives::to_string_q(q_ctx, value_arg)?; 74 | 75 | with_http_request(obj_id, |req| { 76 | // todo args and stuff 77 | log::debug!("setting header in req {} to {}", name_str, value_str); 78 | req.set(name_str.as_str(), value_str.as_str()); 79 | }); 80 | Ok(quickjs_utils::new_null_ref()) 81 | }) 82 | .method("send", |q_ctx, obj_id, args| { 83 | trace!("Request::send"); 84 | 85 | // first arg can be object or string or byte[](UInt8Array) 86 | let content_opt: Option = if !args.is_empty() { 87 | let arg = &args[0]; 88 | let s = if arg.is_object() { 89 | let val_ref = json::stringify_q(q_ctx, arg, None).ok().unwrap(); 90 | primitives::to_string_q(q_ctx, &val_ref).ok().unwrap() 91 | } else { 92 | functions::call_to_string_q(q_ctx, arg).ok().unwrap() 93 | }; 94 | 95 | Some(s) 96 | } else { 97 | None 98 | }; 99 | 100 | let mut req_clone = with_http_request(obj_id, |req| req.build()); 101 | 102 | promises::new_resolving_promise( 103 | q_ctx, 104 | move || { 105 | // producer, make request here and return result 106 | 107 | let response = if let Some(content) = content_opt { 108 | req_clone.send_string(content.as_str()) 109 | } else { 110 | req_clone.call() 111 | }; 112 | 113 | if response.ok() { 114 | Ok(response) 115 | } else { 116 | Err(response 117 | .into_string() 118 | .expect("could not get response error as string")) 119 | } 120 | }, 121 | |q_ctx, response_obj| { 122 | // put res in autoidmap and return new proxy instance here 123 | 124 | response::reg_instance( 125 | q_ctx, 126 | UreqResponseWrapper { 127 | delegate: Some(response_obj), 128 | }, 129 | ) 130 | }, 131 | ) 132 | }) 133 | .install(q_ctx, false) 134 | } 135 | -------------------------------------------------------------------------------- /src/features/require.rs: -------------------------------------------------------------------------------- 1 | //! require 2 | //! 3 | //! this mod implements a require method which can be used to load CommonJS modules 4 | //! 5 | //! It uses the available ScriptModuleLoader instances in QuickJSRuntime 6 | //! 7 | 8 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 9 | use quickjs_runtime::jsutils::{JsError, JsValueType, Script}; 10 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 11 | use quickjs_runtime::quickjsruntimeadapter::QuickJsRuntimeAdapter; 12 | use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 13 | 14 | pub fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 15 | // todo.. this should utilize the script module loaders in order to obtain the source, then use a 'require' function in js to do the actual loading.. 16 | builder.runtime_facade_init_hook(|rt| { 17 | // todo, impl with native function.. like now 18 | 19 | rt.loop_sync_mut(|js_rt| { 20 | js_rt.add_realm_init_hook(|_js_rt, realm| { 21 | //let global = get_global_q(q_ctx); 22 | //let require_func = 23 | // new_native_function_q(q_ctx, "require", Some(require), 1, false)?; 24 | //set_property2_q(q_ctx, &global, "require", &require_func, 0)?; 25 | realm.install_function(&[], "require", require, 1) 26 | }) 27 | })?; 28 | 29 | Ok(()) 30 | }) 31 | } 32 | 33 | const DEFAULT_EXTENSIONS: &[&str] = &["js", "mjs", "ts", "mts"]; 34 | 35 | fn require( 36 | runtime: &QuickJsRuntimeAdapter, 37 | realm: &QuickJsRealmAdapter, 38 | _this_val: &QuickJsValueAdapter, 39 | args: &[QuickJsValueAdapter], 40 | ) -> Result { 41 | if args.len() != 1 || args[0].get_js_type() != JsValueType::String { 42 | Err(JsError::new_str( 43 | "require requires a single string argument", 44 | )) 45 | } else { 46 | let name = args[0].to_string()?; 47 | 48 | let mut cur_path = realm 49 | .get_script_or_module_name() 50 | .ok() 51 | .unwrap_or_else(|| "file:///node_modules/foo.js".to_string()); 52 | 53 | // * if name does not start with / or ./ or ../ then use node_modules ref_path (if ref_path is file:///??) 54 | // todo , where do i cache these? a shutdown hook on a QuickJsContext would be nice to clear my own caches 55 | // much rather have a q_ctx.cache_region("").cache(id, obj) 56 | 57 | // see https://nodejs.org/en/knowledge/getting_started/what_is_require 58 | // * todo 2 support for directories, and then greco_jspreproc.js or package.json? 59 | 60 | // hmm if a module is loaded from https://somegit.somesite.com/scripts/kewlStuff.js and that does a require.. do we look in node_modules on disk? 61 | if !(name.contains("://") 62 | || name.starts_with("./") 63 | || name.starts_with("../") 64 | || name.starts_with('/')) 65 | { 66 | cur_path = format!("file:///node_modules/{name}/foo.js"); 67 | } 68 | 69 | log::debug!("require: {} -> {}", cur_path, name); 70 | 71 | let module_script_opt = (|| { 72 | let opt = runtime.load_module_script(cur_path.as_str(), name.as_str()); 73 | if opt.is_some() { 74 | return opt; 75 | } 76 | for ext in DEFAULT_EXTENSIONS { 77 | let opt = runtime.load_module_script( 78 | cur_path.as_str(), 79 | format!("{}.{}", name.as_str(), ext).as_str(), 80 | ); 81 | if opt.is_some() { 82 | return opt; 83 | } 84 | } 85 | 86 | // see if index.js exists 87 | let mut base_name = name.clone(); 88 | if let Some(rpos) = base_name.rfind('/') { 89 | let _ = base_name.split_off(rpos + 1); 90 | } else { 91 | base_name = "".to_string(); 92 | } 93 | let opt = runtime.load_module_script( 94 | cur_path.as_str(), 95 | format!("{}{}", base_name, "index.js").as_str(), 96 | ); 97 | if opt.is_some() { 98 | return opt; 99 | } 100 | 101 | None 102 | })(); 103 | 104 | if let Some(module_script) = module_script_opt { 105 | // todo need to wrap as ES6 module so ScriptOrModuleName is sound for children 106 | log::debug!("found module script at {}", module_script.get_path()); 107 | 108 | let wrapped_eval_code = format!( 109 | "(function(){{const module = {{exports:{{}}}};let exports = module.exports;{{{}\n}}; return(module.exports);}}())", 110 | module_script.get_code() 111 | ); 112 | let eval_res = realm.eval(Script::new( 113 | module_script.get_path(), 114 | wrapped_eval_code.as_str(), 115 | )); 116 | eval_res 117 | } else { 118 | log::error!("module not found: {} -> {}", cur_path, name); 119 | Err(JsError::new_string(format!( 120 | "module not found: {cur_path} -> {name}" 121 | ))) 122 | } 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | 129 | #[test] 130 | fn test_eval() {} 131 | } 132 | -------------------------------------------------------------------------------- /src/features/js_fetch/mod.rs: -------------------------------------------------------------------------------- 1 | //! fetch implementation 2 | 3 | use crate::features::js_fetch::spec::{do_fetch, FetchInit}; 4 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 5 | use quickjs_runtime::facades::QuickJsRuntimeFacade; 6 | use quickjs_runtime::jsutils::{JsError, JsValueType}; 7 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 8 | 9 | mod proxies; 10 | pub mod spec; 11 | 12 | pub fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 13 | builder.runtime_facade_init_hook(impl_for_rt) 14 | } 15 | 16 | pub fn impl_for_rt(runtime: &QuickJsRuntimeFacade) -> Result<(), JsError> { 17 | runtime.loop_sync_mut(|rta| rta.add_realm_init_hook(|_rt, realm| impl_for(realm))) 18 | } 19 | 20 | pub fn impl_for(realm: &QuickJsRealmAdapter) -> Result<(), JsError> { 21 | realm.install_function( 22 | &[], 23 | "fetch", 24 | |_rt, realm, _this_obj, args| { 25 | // 26 | // convert vals to fetch options here, make fetch options Send 27 | //arg0 = url: String 28 | //arg1 = data: Object 29 | //if arg0 is not a string the returned promise will reject 30 | let url: Option = 31 | if !args.is_empty() && args[0].get_js_type() == JsValueType::String { 32 | Some(args[0].to_string()?) 33 | } else { 34 | None 35 | }; 36 | let fetch_init: FetchInit = FetchInit::from_js_object(realm, args.get(1))?; 37 | 38 | realm.create_resolving_promise_async( 39 | // 40 | // do request here and return result as fetch objects 41 | do_fetch(url, fetch_init), 42 | |realm, res| { 43 | // convert result fetch objects to JsValueAdapter here 44 | res.to_js_value(realm) 45 | }, 46 | ) 47 | }, 48 | 2, 49 | )?; 50 | 51 | proxies::impl_for(realm) 52 | } 53 | 54 | #[cfg(test)] 55 | pub mod tests { 56 | use crate::features::js_fetch::impl_for_rt; 57 | use crate::tests::init_test_greco_rt; 58 | use futures::executor::block_on; 59 | use quickjs_runtime::jsutils::Script; 60 | use quickjs_runtime::values::JsValueFacade; 61 | 62 | #[test] 63 | fn test_fetch_generic() { 64 | let rt = init_test_greco_rt(); 65 | 66 | #[allow(clippy::ok_expect)] 67 | impl_for_rt(&rt).ok().expect("init failed"); 68 | 69 | let fetch_fut = rt.eval( 70 | None, 71 | Script::new("test_fetch_gen.js", "let testFunc = async function() {console.log(1); let fetchRes = await fetch('https://httpbin.org/anything', {headers: {myHeader: ['a', 'b']}}); let text = await fetchRes.text(); return text;}; testFunc()"), 72 | ); 73 | let res = block_on(fetch_fut); 74 | match res { 75 | Ok(val) => match val { 76 | JsValueFacade::JsPromise { cached_promise } => { 77 | let res_fut = cached_promise.get_promise_result(); 78 | let fetch_res = block_on(res_fut); 79 | match fetch_res { 80 | Ok(v) => match v { 81 | Ok(resolved) => { 82 | //assert_eq!(resolved.js_get_value_type(), JsValueType::String); 83 | println!("resolved to string: {}", resolved.stringify()); 84 | } 85 | Err(rejected) => { 86 | panic!("promise was rejected: {}", rejected.stringify()); 87 | } 88 | }, 89 | Err(e) => { 90 | panic!("fetch failed {}", e) 91 | } 92 | } 93 | } 94 | _ => { 95 | panic!("result was not a promise") 96 | } 97 | }, 98 | Err(e) => { 99 | panic!("script failed: {}", e); 100 | } 101 | } 102 | } 103 | 104 | /*#[test] 105 | fn test_chart() { 106 | let rt = init_test_greco_rt(); 107 | 108 | impl_for_rt(&rt).ok().expect("init failed"); 109 | 110 | let fetch_fut = rt.eval( 111 | None, 112 | Script::new("test_fetch_gen.js", r#" 113 | let testFunc = async function() { 114 | console.log(1); 115 | let body = { 116 | "chart": { 117 | "chartOptions": {"as": "svg"} 118 | } 119 | }; 120 | let fetchRes = await fetch('http://192.168.10.43:8055/charts/line', {body: JSON.stringify(body), headers: {"Content-Type": ['application/json']}}); 121 | let text = await fetchRes.text(); 122 | return text; 123 | }; 124 | testFunc() 125 | "#), 126 | ); 127 | let res = block_on(fetch_fut); 128 | match res { 129 | Ok(val) => match val { 130 | JsValueFacade::JsPromise { cached_promise } => { 131 | let res_fut = cached_promise.get_promise_result(); 132 | let fetch_res = block_on(res_fut); 133 | match fetch_res { 134 | Ok(v) => match v { 135 | Ok(resolved) => { 136 | //assert_eq!(resolved.js_get_value_type(), JsValueType::String); 137 | println!("resolved to string: {}", resolved.stringify()); 138 | } 139 | Err(rejected) => { 140 | panic!("promise was rejected: {}", rejected.stringify()); 141 | } 142 | }, 143 | Err(e) => { 144 | panic!("fetch failed {}", e) 145 | } 146 | } 147 | } 148 | _ => { 149 | panic!("result was not a promise") 150 | } 151 | }, 152 | Err(e) => { 153 | panic!("script failed: {}", e); 154 | } 155 | } 156 | }*/ 157 | } 158 | -------------------------------------------------------------------------------- /src/modules/com/http/client.rs: -------------------------------------------------------------------------------- 1 | use crate::modules::com::http::request; 2 | use hirofa_utils::js_utils::JsError; 3 | use log::trace; 4 | use quickjs_runtime::quickjs_utils; 5 | use quickjs_runtime::quickjs_utils::primitives; 6 | use quickjs_runtime::reflection::Proxy; 7 | use quickjs_runtime::valueref::JSValueRef; 8 | use std::cell::RefCell; 9 | use std::collections::HashMap; 10 | use std::time::Duration; 11 | 12 | type HttpClientType = ureq::Agent; 13 | 14 | thread_local! { 15 | static HTTP_CLIENT_INSTANCES: RefCell> = 16 | RefCell::new(HashMap::new()); 17 | 18 | } 19 | 20 | fn with_http_client(instance_id: &usize, consumer: C) -> R 21 | where 22 | C: Fn(&mut HttpClientType) -> R, 23 | { 24 | HTTP_CLIENT_INSTANCES.with(move |instances_rc| { 25 | let instances = &mut *instances_rc.borrow_mut(); 26 | let i = instances 27 | .get_mut(instance_id) 28 | .expect("not a valid instance id"); 29 | consumer(i) 30 | }) 31 | } 32 | 33 | pub(crate) fn init_http_client_proxy( 34 | q_ctx: &QuickJsContext, 35 | namespace: Vec<&'static str>, 36 | ) -> Result { 37 | Proxy::new() 38 | .name("Client") 39 | .namespace(namespace) 40 | .constructor(|_q_js_rt, instance_id, _args| { 41 | trace!("Client::constructor"); 42 | HTTP_CLIENT_INSTANCES.with(|instances_rc| { 43 | let instances = &mut *instances_rc.borrow_mut(); 44 | 45 | let agent = ureq::agent(); 46 | 47 | instances.insert(instance_id, agent); 48 | 49 | Ok(()) 50 | }) 51 | }) 52 | .finalizer(|_q_ctx, instance_id| { 53 | trace!("Client::finalizer"); 54 | HTTP_CLIENT_INSTANCES.with(|instances_rc| { 55 | let instances = &mut *instances_rc.borrow_mut(); 56 | instances.remove(&(instance_id as usize)); 57 | }) 58 | }) 59 | .method("basicAuth", |q_ctx, obj_id, args| { 60 | trace!("Client::basicAuth {}", obj_id); 61 | with_http_client(obj_id, |client| { 62 | // do something with client 63 | 64 | if args.len() != 2 { 65 | return Err(JsError::new_string(format!( 66 | "basicAuth requires 2 arguments, got {}", 67 | args.len() 68 | ))); 69 | } 70 | 71 | let user_arg = &args[0]; 72 | let pass_arg = &args[1]; 73 | 74 | if !user_arg.is_string() { 75 | return Err(JsError::new_str( 76 | "basicAuth requires a String as first argument", 77 | )); 78 | } 79 | if !pass_arg.is_string() { 80 | return Err(JsError::new_str( 81 | "basicAuth requires a String as second argument", 82 | )); 83 | } 84 | 85 | let user = primitives::to_string_q(q_ctx, &user_arg)?; 86 | let pass = primitives::to_string_q(q_ctx, &pass_arg)?; 87 | 88 | client.auth(user.as_str(), pass.as_str()); 89 | Ok(quickjs_utils::new_null_ref()) 90 | }) 91 | }) 92 | .method("setHeader", |q_ctx, obj_id, args| { 93 | trace!("Client::setHeader {}", obj_id); 94 | with_http_client(obj_id, |client| { 95 | // do something with client 96 | 97 | if args.len() != 2 { 98 | return Err(JsError::new_string(format!( 99 | "setHeader requires 2 arguments, got {}", 100 | args.len() 101 | ))); 102 | } 103 | 104 | let header_arg = &args[0]; 105 | let value_arg = &args[1]; 106 | 107 | if !header_arg.is_string() { 108 | return Err(JsError::new_str( 109 | "setHeader requires a String as first argument", 110 | )); 111 | } 112 | if !value_arg.is_string() { 113 | return Err(JsError::new_str( 114 | "setHeader requires a String as second argument", 115 | )); 116 | } 117 | 118 | let header = primitives::to_string_q(q_ctx, &header_arg)?; 119 | let value = primitives::to_string_q(q_ctx, &value_arg)?; 120 | 121 | client.set(header.as_str(), value.as_str()); 122 | Ok(quickjs_utils::new_null_ref()) 123 | }) 124 | }) 125 | .method("request", |q_ctx, http_client_obj_id, args| { 126 | if args.len() != 2 { 127 | return Err(JsError::new_string( 128 | "request method requires 2 string arguments: a request method and a path" 129 | .to_string(), 130 | )); 131 | } 132 | 133 | trace!("Client::request"); 134 | 135 | let path_val = &args[1]; 136 | let method_val = &args[0]; 137 | 138 | if !path_val.is_string() { 139 | return Err(JsError::new_str("path argument should be a string")); 140 | } 141 | if !method_val.is_string() { 142 | return Err(JsError::new_str("method argument should be a string")); 143 | } 144 | 145 | let method = primitives::to_string_q(q_ctx, method_val)?; 146 | let path = primitives::to_string_q(q_ctx, path_val)?; 147 | 148 | // todo const / webdav methods 149 | let methods = vec![ 150 | "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "TRACE", "PATCH", 151 | ]; 152 | if !methods.contains(&method.as_str()) { 153 | return Err(JsError::new_string(format!("invalid method: {}", method))); 154 | } 155 | 156 | with_http_client(http_client_obj_id, |client| { 157 | let mut request_obj = client.request(method.as_str(), path.as_str()); 158 | request_obj.timeout(Duration::from_secs(10)); 159 | request_obj.timeout_connect(5000); 160 | request::reg_instance(q_ctx, request_obj) 161 | }) 162 | }) 163 | .install(q_ctx, false) 164 | } 165 | -------------------------------------------------------------------------------- /src/modules/com/http/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Http module 2 | //! 3 | //! The http module provides a more manageable httpclient than the fetch api can provide 4 | //! 5 | //! # exports 6 | //! 7 | //! ## Client 8 | //! 9 | //! * retains cookies 10 | //! * default http headers 11 | //! 12 | //! ### setHeader 13 | //! 14 | //! ### basicAuth 15 | //! 16 | //! ## Request 17 | //! 18 | //! * setHeader(name, val) 19 | //! * async send() 20 | //! 21 | //! ## Response 22 | //! 23 | //! * get text() 24 | //! 25 | //! ## do requests 26 | //! 27 | //! ```javascript 28 | //! async function testHttp() { 29 | //! let http_mod = await import("greco://http"); 30 | //! let client = new http_mod.Client(); 31 | //! client.basicAuth("userA", "passB"); 32 | //! client.setHeader("X-Api-Key", "12345"); 33 | //! 34 | //! let req = client.request("GET", "https://foo.com"); 35 | //! req.setHeader("headerA", "a"); 36 | //! req.setHeader("headerB", "b"); 37 | //! 38 | //! let response = await req.send(); 39 | //! let txt = response.text; 40 | //! console.log("got response: %s", txt); 41 | //! } 42 | //! ``` 43 | //! 44 | 45 | use hirofa_utils::js_utils::facades::JsRuntimeBuilder; 46 | use hirofa_utils::js_utils::modules::NativeModuleLoader; 47 | use hirofa_utils::js_utils::JsError; 48 | 49 | mod client; 50 | mod request; 51 | mod response; 52 | 53 | struct HttpModuleLoader {} 54 | 55 | impl NativeModuleLoader for HttpModuleLoader { 56 | fn has_module(&self, _q_ctx: &QuickJsContext, module_name: &str) -> bool { 57 | module_name.eq("greco://http") 58 | } 59 | 60 | fn get_module_export_names(&self, _q_ctx: &QuickJsContext, _module_name: &str) -> Vec<&str> { 61 | vec!["Client"] 62 | } 63 | 64 | fn get_module_exports( 65 | &self, 66 | q_ctx: &QuickJsContext, 67 | _module_name: &str, 68 | ) -> Vec<(&str, JSValueRef)> { 69 | init_exports(q_ctx).ok().expect("init http exports failed") 70 | } 71 | } 72 | 73 | pub(crate) fn init(builder: &mut B) { 74 | builder.js_native_module_loader(Box::new(HttpModuleLoader {})); 75 | } 76 | 77 | fn init_exports(q_ctx: &QuickJsContext) -> Result, JsError> { 78 | let namespace = vec!["greco", "com", "http"]; 79 | let http_client_proxy_class = client::init_http_client_proxy(q_ctx, namespace.clone())?; 80 | let http_request_proxy_class = request::init_http_request_proxy(q_ctx, namespace.clone())?; 81 | let http_response_proxy_class = response::init_http_response_proxy(q_ctx, namespace.clone())?; 82 | 83 | Ok(vec![ 84 | ("Client", http_client_proxy_class), 85 | ("Request", http_request_proxy_class), 86 | ("Response", http_response_proxy_class), 87 | ]) 88 | } 89 | 90 | #[cfg(test)] 91 | pub mod tests { 92 | 93 | use crate::tests::init_test_greco_rt; 94 | use hirofa_utils::js_utils::Script; 95 | 96 | #[test] 97 | fn test_http_client() { 98 | let rt = init_test_greco_rt(); 99 | let _ = rt 100 | .eval_sync(Script::new( 101 | "test_http_client1.es", 102 | "\ 103 | this.test_http_client = async function() {\ 104 | try {\ 105 | let http_mod = await import('greco://http');\ 106 | let test_http_client = new http_mod.Client();\ 107 | test_http_client.setHeader('a', 'b');\ 108 | let req = test_http_client.request('GET', 'https://httpbin.org/anything');\ 109 | req.setHeader('foo', 'bar');\ 110 | let response = await req.send();\ 111 | let txt = response.text;\ 112 | return('response text = ' + txt);\ 113 | } catch(ex){\ 114 | console.error('test_http_client failed at %s', '' + ex);\ 115 | throw Error('test_http_client failed at ' + ex); 116 | }\ 117 | }\ 118 | ", 119 | )) 120 | .ok() 121 | .expect("script failed"); 122 | 123 | let esvf_prom = rt 124 | .call_function_sync(vec![], "test_http_client", vec![]) 125 | .ok() 126 | .expect("func invoc failed"); 127 | let esvf_res = esvf_prom.get_promise_result_sync(); 128 | match esvf_res { 129 | Ok(o) => { 130 | assert!(o.is_string()); 131 | let res_str = o.get_str(); 132 | log::debug!("response text = {}", res_str); 133 | assert!(res_str.starts_with("response text = ")); 134 | } 135 | Err(e) => { 136 | panic!("failed {}", e.get_str()); 137 | } 138 | } 139 | } 140 | 141 | #[test] 142 | fn test_http_client_post() { 143 | let rt = init_test_greco_rt(); 144 | let _ = rt 145 | .eval_sync(Script::new( 146 | "test_http_client1.es", 147 | "\ 148 | this.test_http_client = async function() {\ 149 | let http_mod = await import('greco://http');\ 150 | let test_http_client = new http_mod.Client();\ 151 | test_http_client.setHeader('a', 'b');\ 152 | let req = test_http_client.request('POST', 'https://httpbin.org/post');\ 153 | req.setHeader('foo', 'bar');\ 154 | let response = await req.send('hello posty world');\ 155 | let txt = response.text;\ 156 | return('response text = ' + txt);\ 157 | }\ 158 | ", 159 | )) 160 | .ok() 161 | .expect("scriopt failed"); 162 | 163 | let esvf_prom = rt 164 | .call_function_sync(vec![], "test_http_client", vec![]) 165 | .ok() 166 | .expect("func invoc faled"); 167 | let esvf_res = esvf_prom.get_promise_result_sync(); 168 | match esvf_res { 169 | Ok(o) => { 170 | assert!(o.is_string()); 171 | let res_str = o.get_str(); 172 | log::debug!("response text = {}", res_str); 173 | assert!(res_str.starts_with("response text = ")); 174 | assert!(res_str.contains("\"data\": \"hello posty world\"")); 175 | assert!(res_str.contains("\"A\": \"b\"")); 176 | } 177 | Err(e) => { 178 | panic!("failed {}", e.get_str()); 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/preprocessors/cpp.rs: -------------------------------------------------------------------------------- 1 | //! cpp style preprocessor 2 | //! 3 | //! this can be used to define c-like preprocessing instructions in javascript 4 | //! ```javascript 5 | //! function do_stuff(input) { 6 | //! #ifdef $GRECO_DEBUG 7 | //! if (input.includes('booh')) { 8 | //! throw Error('input should not include booh'); 9 | //! } 10 | //! #endif 11 | //! console.log('got input %s', input); 12 | //! } 13 | //! ``` 14 | //! 15 | //! it used the gpp crate and docs on how it works are here https://docs.rs/gpp/0.6.0/gpp 16 | //! 17 | //! by default GreenCopperRuntime conditionally sets the $GRECO_DEBUG, $GRECO_TEST and $GRECO_RELEASE 18 | //! you can also add all current env_vars so in script you can use ```let path = "$PATH";```; 19 | //! 20 | //! # Example 21 | //! ```rust 22 | //! use green_copper_runtime::preprocessors::cpp::CppPreProcessor; 23 | //! use quickjs_runtime::builder::QuickJsRuntimeBuilder; 24 | //! use quickjs_runtime::jsutils::Script; 25 | //! 26 | //! let cpp = CppPreProcessor::new().default_extensions().env_vars(); 27 | //! let rt = QuickJsRuntimeBuilder::new().script_pre_processor(cpp).build(); 28 | //! 29 | //! let path = rt.eval_sync(None, Script::new("test.js", "let p = '$PATH'; p")).ok().expect("script failed"); 30 | //! assert!(!path.get_str().is_empty()); 31 | //! assert_ne!(path.get_str(), "$PATH"); 32 | //! 33 | //! ``` 34 | //! 35 | 36 | use gpp::{process_str, Context}; 37 | use quickjs_runtime::jsutils::JsError; 38 | use quickjs_runtime::jsutils::{Script, ScriptPreProcessor}; 39 | use std::cell::RefCell; 40 | use std::env; 41 | 42 | pub struct CppPreProcessor { 43 | ctx: RefCell, 44 | extensions: Vec<&'static str>, 45 | } 46 | 47 | impl Default for CppPreProcessor { 48 | fn default() -> Self { 49 | Self::new() 50 | } 51 | } 52 | 53 | impl CppPreProcessor { 54 | pub fn new() -> Self { 55 | let mut ret = Self { 56 | ctx: RefCell::new(Context::new()), 57 | extensions: vec![], 58 | }; 59 | 60 | #[cfg(debug_assertions)] 61 | { 62 | ret = ret.def("GRECO_DEBUG", "true"); 63 | } 64 | #[cfg(test)] 65 | { 66 | ret = ret.def("GRECO_TEST", "true"); 67 | } 68 | #[cfg(not(any(debug_assertions, test)))] 69 | { 70 | ret = ret.def("GRECO_RELEASE", "true"); 71 | } 72 | 73 | ret 74 | } 75 | /// add a def 76 | pub fn def(self, key: &str, value: &str) -> Self { 77 | { 78 | let ctx = &mut *self.ctx.borrow_mut(); 79 | 80 | ctx.macros.insert(format!("${{{key}}}"), value.to_string()); 81 | ctx.macros.insert(format!("${key}"), value.to_string()); 82 | ctx.macros.insert(format!("__{key}"), value.to_string()); 83 | } 84 | self 85 | } 86 | /// add a supported extension e.g. js/mjs/ts/mts/es/mes 87 | pub fn extension(mut self, ext: &'static str) -> Self { 88 | self.extensions.push(ext); 89 | self 90 | } 91 | 92 | pub fn env_vars(mut self) -> Self { 93 | log::debug!("adding env vars"); 94 | for (key, value) in env::vars() { 95 | log::debug!("adding env var {} = {}", key, value); 96 | self = self.def(key.as_str(), value.as_str()); 97 | } 98 | self 99 | } 100 | 101 | /// add default extensions : js/mjs/ts/mts/es/mes 102 | pub fn default_extensions(self) -> Self { 103 | self.extension("es") 104 | .extension("mes") 105 | .extension("js") 106 | .extension("mjs") 107 | .extension("ts") 108 | .extension("mts") 109 | } 110 | } 111 | 112 | impl ScriptPreProcessor for CppPreProcessor { 113 | fn process(&self, script: &mut Script) -> Result<(), JsError> { 114 | if "CppPreProcessor.not_es".eq(script.get_path()) { 115 | return Ok(()); 116 | } 117 | 118 | log::debug!("CppPreProcessor > {}", script.get_path()); 119 | //println!("CppPreProcessor > {}", script.get_path()); 120 | 121 | let src = script.get_code(); 122 | 123 | let res = process_str(src, &mut self.ctx.borrow_mut()) 124 | .map_err(|e| JsError::new_string(format!("{e:?}")))?; 125 | 126 | script.set_code(res); 127 | Ok(()) 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | 134 | use crate::preprocessors::cpp::CppPreProcessor; 135 | use crate::tests::init_test_greco_rt; 136 | use futures::executor::block_on; 137 | use quickjs_runtime::jsutils::{Script, ScriptPreProcessor}; 138 | use quickjs_runtime::values::JsValueFacade; 139 | 140 | #[test] 141 | fn test_ifdef_script_only() { 142 | let cpp = CppPreProcessor::new() 143 | .default_extensions() 144 | .env_vars() 145 | .def("TEST_AUTOMATION", "true"); 146 | 147 | let mut script = Script::new( 148 | "testifdef.js", 149 | r#" 150 | #ifdef $TEST_AUTOMATION 151 | 1 152 | #else 153 | 2 154 | #endif 155 | "#, 156 | ); 157 | cpp.process(&mut script).unwrap(); 158 | assert_eq!("\n1\n", script.get_code()); 159 | } 160 | 161 | #[test] 162 | fn test_ifdef() { 163 | let rt = init_test_greco_rt(); 164 | let fut = rt.eval( 165 | None, 166 | Script::new( 167 | "test.es", 168 | "((function(){\n\ 169 | #ifdef HELLO\n\ 170 | return 111;\n\ 171 | #elifdef $GRECO_DEBUG\n\ 172 | return 123;\n\ 173 | #else\n\ 174 | return 222;\n\ 175 | #endif\n\ 176 | })());", 177 | ), 178 | ); 179 | let res = block_on(fut); 180 | let num = match res { 181 | Ok(e) => e, 182 | Err(err) => { 183 | panic!("{}", err); 184 | } 185 | }; 186 | if let JsValueFacade::I32 { val } = num { 187 | assert_eq!(val, 123); 188 | } else { 189 | panic!("not an i32") 190 | } 191 | } 192 | 193 | #[test] 194 | fn test_vars() { 195 | let rt = init_test_greco_rt(); 196 | let fut = rt.eval( 197 | None, 198 | Script::new( 199 | "test.es", 200 | "((function(){\n\ 201 | return('p=${PATH}');\n\ 202 | })());", 203 | ), 204 | ); 205 | let res = block_on(fut); 206 | let val = match res { 207 | Ok(e) => e, 208 | Err(err) => { 209 | panic!("{}", err); 210 | } 211 | }; 212 | 213 | if let JsValueFacade::String { val } = val { 214 | assert_ne!(&*val, "${PATH}"); 215 | assert!(!val.is_empty()); 216 | } else { 217 | panic!("not a string") 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /modules/io/gpio/stepper.mes: -------------------------------------------------------------------------------- 1 | import {Assertions as assert} from 'https://raw.githubusercontent.com/HiRoFa/GreenCopperRuntime/main/modules/utils/assertions.mes'; 2 | import {PinSet} from 'greco://gpio'; 3 | 4 | // todo init with a StepperDriver class, (FourPinStepperDriver(a, b, c, d), ThreePinStepperDriver(enable, dir, pulse), VirtualStepperDriver) 5 | 6 | export class StepperDriver { 7 | constructor() { 8 | 9 | } 10 | async init() { 11 | 12 | } 13 | async step(sequenceCount = 1, forward = true) { 14 | 15 | } 16 | } 17 | 18 | export class FourPinGPIOStepperDriver extends StepperDriver { 19 | // todo sequence and delay should be setter/getter instead of something you pass to step method 20 | constructor(chip = '/dev/gpiochip0', pinNum0, pinNum1, pinNum2, pinNum3, sequencesPerRevolution = 509.4716) { 21 | super(); 22 | } 23 | async init() { 24 | 25 | } 26 | async step(sequenceCount = 1, forward = true) { 27 | 28 | } 29 | } 30 | 31 | export class ThreePinGPIOStepperDriver extend StepperDriver { 32 | constructor(chip = '/dev/gpiochip0', pinNumEnable, pinNumPulse, pinNumDirection, sequencesPerRevolution = 509.4716) { 33 | super(); 34 | } 35 | async init() { 36 | 37 | } 38 | async step(sequenceCount = 1, forward = true) { 39 | 40 | } 41 | } 42 | 43 | export class VirtualStepperDriver extends StepperDriver { 44 | 45 | } 46 | 47 | export class Stepper { 48 | constructor(pinSet, sequencesPerRevolution) { 49 | this.pinSet = pinSet; 50 | this.sequencesPerRevolution = sequencesPerRevolution; 51 | this.setZero(); 52 | } 53 | 54 | /** 55 | * init a new Stepper 56 | */ 57 | static async init(chip = '/dev/gpiochip0', pinNum0, pinNum1, pinNum2, pinNum3, sequencesPerRevolution = 509.4716) { 58 | 59 | assert.is_string(chip, "chip should be a string like '/dev/gpiochip0'"); 60 | assert.is_number(pinNum0, "pinNum0 should be a number"); 61 | assert.is_number(pinNum1, "pinNum1 should be a number"); 62 | assert.is_number(pinNum2, "pinNum2 should be a number"); 63 | assert.is_number(pinNum3, "pinNum3 should be a number"); 64 | 65 | let pinSet = new PinSet(); 66 | let instance = new this(pinSet, sequencesPerRevolution); 67 | 68 | await instance.pinSet.init(chip, 'out', [pinNum0, pinNum1, pinNum2, pinNum3]); 69 | 70 | return instance; 71 | 72 | } 73 | 74 | /** 75 | * set the current position as the "zero" position 76 | **/ 77 | setZero() { 78 | this.numSequenceForwarded = 0; 79 | } 80 | 81 | /** 82 | * return to the "zero" position 83 | **/ 84 | async zero() { 85 | if (this.numSequenceForwarded >= 0) { 86 | return this.step(this.numSequenceForwarded, false); 87 | } else { 88 | return this.step(-this.numSequenceForwarded, true); 89 | } 90 | } 91 | 92 | /** 93 | * move the motor 94 | * @param sequenceCount the number of sequences to run (a single sequence is 4 steps) 95 | * @param forward true for forward false for backward movement 96 | * @param stepDelay the number of milliseconds to wait between each step 97 | * @param sequence Stepper.SINGLE_STEP, Stepper.DOUBLE_STEP or Stepper.HALF_STEP 98 | */ 99 | async step(sequenceCount = 1, forward = true, stepDelay = 2, sequence = 1) { 100 | 101 | assert.is_number(sequenceCount, "sequenceCount should be a positive number"); 102 | assert.is_true(sequenceCount >= 0, "sequenceCount should be a positive number"); 103 | 104 | assert.is_boolean(forward, "forward should be boolean"); 105 | 106 | assert.is_number(stepDelay, "stepDelay should be a positive number"); 107 | assert.is_true(stepDelay >= 0, "stepDelay should be a positive number"); 108 | 109 | assert.is_true(sequence === 0 || sequence === 1 || sequence === 2, "sequence should be one of Stepper.SINGLE_STEP, Stepper.DOUBLE_STEP or Stepper.HALF_STEP"); 110 | 111 | let s; 112 | switch (sequence) { 113 | case Stepper.SINGLE_STEP: 114 | s = forward?Stepper.SEQUENCE_SINGLE_FORWARD:Stepper.SEQUENCE_SINGLE_BACKWARD; 115 | break; 116 | case Stepper.DOUBLE_STEP: 117 | s = forward?Stepper.SEQUENCE_DOUBLE_FORWARD:Stepper.SEQUENCE_DOUBLE_BACKWARD; 118 | break; 119 | case Stepper.HALF_STEP: 120 | s = forward?Stepper.SEQUENCE_HALF_FORWARD:Stepper.SEQUENCE_HALF_BACKWARD; 121 | break; 122 | } 123 | return this.pinSet.sequence(s, stepDelay, Math.round(sequenceCount)) 124 | .then((res) => { 125 | this.numSequenceForwarded += forward?sequenceCount:-sequenceCount; 126 | return res; 127 | }); 128 | } 129 | 130 | /** 131 | * move the motor 132 | * @param revolutions the number of revolutions to make (a single sequence is 4 or 8 steps depending on the sequence) 133 | * @param forward true for forward false for backward movement 134 | * @param stepDelay the number of milliseconds to wait between each step 135 | * @param sequence Stepper.SINGLE_STEP, Stepper.DOUBLE_STEP or Stepper.HALF_STEP 136 | */ 137 | async rotate(revolutions = 1, forward = true, stepDelay = 2, sequence = 1) { 138 | 139 | assert.is_number(revolutions, "revolutions should be a positive number"); 140 | assert.is_true(revolutions >= 0, "revolutions should be a positive number"); 141 | 142 | let sequenceCount = this.sequencesPerRevolution * revolutions; 143 | return this.step(sequenceCount, forward, stepDelay, sequence); 144 | } 145 | 146 | /** 147 | * move the motor a number of degrees 148 | * @param degrees the number of degrees to move (use a negative number to move backwards) 149 | * @param stepDelay the number of milliseconds to wait between each step 150 | * @param sequence Stepper.SINGLE_STEP, Stepper.DOUBLE_STEP or Stepper.HALF_STEP 151 | */ 152 | async rotateDegrees(degrees = 180, stepDelay = 2, sequence = 1) { 153 | 154 | assert.is_number(degrees, "degrees should be a number"); 155 | 156 | let sequenceCount = (this.sequencesPerRevolution / 360) * degrees; 157 | let forward = true; 158 | if (sequenceCount < 0) { 159 | forward = false; 160 | sequenceCount = -sequenceCount; 161 | } 162 | return this.step(sequenceCount, forward, stepDelay, sequence); 163 | } 164 | } 165 | 166 | Stepper.SINGLE_STEP = 0; 167 | Stepper.DOUBLE_STEP = 1; 168 | Stepper.HALF_STEP = 2; 169 | 170 | Stepper.SEQUENCE_SINGLE_FORWARD = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]; 171 | Stepper.SEQUENCE_DOUBLE_FORWARD = [[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 1], [1, 0, 0, 1]]; 172 | Stepper.SEQUENCE_HALF_FORWARD = [[1, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 1, 1], [0, 0, 0, 1], [1, 0, 0, 1]]; 173 | Stepper.SEQUENCE_SINGLE_BACKWARD = [[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]]; 174 | Stepper.SEQUENCE_DOUBLE_BACKWARD = [[1, 0, 0, 1], [0, 0, 1, 1], [0, 1, 1, 0], [1, 1, 0, 0]]; 175 | Stepper.SEQUENCE_HALF_BACKWARD = [[1, 0, 0, 1], [0, 0, 0, 1], [0, 0, 1, 1], [0, 0, 1, 0], [0, 1, 1, 0], [0, 1, 0, 0], [1, 1, 0, 0], [1, 0, 0, 0]]; -------------------------------------------------------------------------------- /src/modules/parsers/mod.rs: -------------------------------------------------------------------------------- 1 | use csv::Trim; 2 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 3 | use quickjs_runtime::jsutils::jsproxies::JsProxy; 4 | use quickjs_runtime::jsutils::modules::NativeModuleLoader; 5 | use quickjs_runtime::jsutils::JsError; 6 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 7 | use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 8 | use quickjs_runtime::values::JsValueFacade; 9 | use std::str; 10 | 11 | struct ParsersModuleLoader {} 12 | 13 | impl NativeModuleLoader for ParsersModuleLoader { 14 | fn has_module(&self, _realm: &QuickJsRealmAdapter, module_name: &str) -> bool { 15 | module_name.eq("greco://parsers") 16 | } 17 | 18 | fn get_module_export_names( 19 | &self, 20 | _realm: &QuickJsRealmAdapter, 21 | _module_name: &str, 22 | ) -> Vec<&str> { 23 | vec!["CsvParser"] 24 | } 25 | 26 | fn get_module_exports( 27 | &self, 28 | realm: &QuickJsRealmAdapter, 29 | _module_name: &str, 30 | ) -> Vec<(&str, QuickJsValueAdapter)> { 31 | init_exports(realm).expect("init parsers exports failed") 32 | } 33 | } 34 | 35 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 36 | builder.native_module_loader(ParsersModuleLoader {}) 37 | } 38 | 39 | fn init_exports( 40 | realm: &QuickJsRealmAdapter, 41 | ) -> Result, JsError> { 42 | let csv_parser_proxy_class = create_csv_parser_proxy(realm); 43 | let csv_parser_res = realm.install_proxy(csv_parser_proxy_class, false)?; 44 | 45 | Ok(vec![("CsvParser", csv_parser_res)]) 46 | } 47 | 48 | pub(crate) fn create_csv_parser_proxy(_realm: &QuickJsRealmAdapter) -> JsProxy { 49 | JsProxy::new().namespace(&["greco", "parsers"]).name("CsvParser") 50 | .static_method("parse", |_runtime, realm, args| { 51 | 52 | // three args, a string or Uint8array for data, a recordCallBack function and an optional options object 53 | 54 | if args.len() < 3 || !(args[0].is_string() || args[0].is_typed_array()) || !args[1].is_function() || !args[2].is_function() { 55 | Err(JsError::new_str("parse requires 2 or 3 args (data: string | Uint8Array, headersCallBack: (headers: array) => void, recordCallback: (record: array) => void, options: {})")) 56 | } else { 57 | 58 | // get data, func_ref as JsValueFacade, move to producer 59 | 60 | let data = if args[0].is_string() { 61 | args[0].to_string()? 62 | } else { 63 | let buf = realm.copy_typed_array_buffer(&args[0])?; 64 | String::from_utf8(buf).map_err(|e| JsError::new_string(format!("{e:?}")))? 65 | }; 66 | let cb_h_func = realm.to_js_value_facade(&args[1])?; 67 | let cb_r_func = realm.to_js_value_facade(&args[2])?; 68 | 69 | realm.create_resolving_promise_async( async move { 70 | 71 | let mut rdr = csv::ReaderBuilder::new() 72 | .double_quote(true) 73 | .delimiter(b',') 74 | .has_headers(true) 75 | .quoting(true) 76 | .flexible(true) 77 | //.ascii() 78 | .trim(Trim::All) 79 | .from_reader(data.as_bytes()); 80 | 81 | let cached_h_function = if let JsValueFacade::JsFunction { cached_function } = cb_h_func { cached_function } else { panic!("function was not a function") }; 82 | let cached_r_function = if let JsValueFacade::JsFunction { cached_function } = cb_r_func { cached_function } else { panic!("function was not a function") }; 83 | 84 | let headers = rdr.headers().map_err(|e| JsError::new_string(format!("{e:?}")))?; 85 | 86 | log::trace!("greco::parsers::CsvParser headers: {:?}", headers); 87 | 88 | let val: Vec = headers.iter().map(|h| { 89 | JsValueFacade::new_str(h) 90 | }).collect(); 91 | 92 | let _ = cached_h_function.invoke_function( vec![JsValueFacade::Array {val}]).await; 93 | 94 | for result in rdr.records() { 95 | // The iterator yields Result, so we check the 96 | // error here. 97 | let record = result.map_err(|e| JsError::new_string(format!("{e:?}")))?; 98 | 99 | // fill val from record 100 | let val: Vec = record.iter().map(|h| { 101 | JsValueFacade::new_str(h) 102 | }).collect(); 103 | 104 | let jsvf_record = JsValueFacade::Array {val}; 105 | 106 | let _ = cached_r_function.invoke_function( vec![jsvf_record]).await; 107 | 108 | log::trace!("greco::parsers::CsvParser row: {:?}", record); 109 | } 110 | 111 | 112 | Ok(()) 113 | }, |realm, _result| { 114 | realm.create_null() 115 | }) 116 | } 117 | 118 | 119 | }) 120 | } 121 | 122 | #[cfg(test)] 123 | pub mod tests { 124 | use futures::executor::block_on; 125 | //use log::LevelFilter; 126 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 127 | use quickjs_runtime::jsutils::Script; 128 | use quickjs_runtime::values::JsValueFacade; 129 | 130 | #[test] 131 | fn test_csv() { 132 | //simple_logging::log_to_stderr(log::LevelFilter::Info); 133 | 134 | let builder = QuickJsRuntimeBuilder::new(); 135 | let builder = crate::init_greco_rt(builder); 136 | let rt = builder.build(); 137 | 138 | let script = Script::new( 139 | "test_parsers.js", 140 | r#" 141 | 142 | async function test() { 143 | let parsersMod = await import('greco://parsers'); 144 | 145 | let data = '"r 1", "r2", "r3", "r4"\n"a", "b", 1, 2\n"c", "d", 3, 4'; 146 | 147 | let ret = ""; 148 | 149 | await parsersMod.CsvParser.parse(data, (headers) => { 150 | console.log("headers: " + headers.join("-")); 151 | ret += "headers: " + headers.join("-") + "\n"; 152 | }, (row) => { 153 | console.log("row: " + row.join("-")); 154 | ret += "row: " + row.join("-") + "\n" 155 | 156 | }); 157 | console.log("parser done"); 158 | 159 | return ret; 160 | 161 | } 162 | 163 | test() 164 | 165 | "#, 166 | ); 167 | let res: JsValueFacade = block_on(rt.eval(None, script)).ok().expect("script failed"); 168 | 169 | println!("{}", res.stringify()); 170 | if let JsValueFacade::JsPromise { cached_promise } = res { 171 | let p_res = block_on(cached_promise.get_promise_result()) 172 | .ok() 173 | .expect("get prom res failed"); 174 | match p_res { 175 | Ok(jsvf) => { 176 | println!("prom resolved to {}", jsvf.stringify()); 177 | } 178 | Err(e) => { 179 | panic!("prom rejected: {}", e.stringify()); 180 | } 181 | } 182 | } else { 183 | panic!("did not get a promise"); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /modules/io/gpio/servo.mes: -------------------------------------------------------------------------------- 1 | import {Assertions as assert} from 'https://raw.githubusercontent.com/HiRoFa/GreenCopperRuntime/main/modules/utils/assertions.mes'; 2 | import {PinSet} from 'greco://gpio'; 3 | 4 | // todo ServoDrivers 5 | // ServoDriver (SoftPwmDriver, PwmDriver, I2C?, AbstractDriverBoardDriver, PCA9685ServoDriver extends AbstractDriverBoardDriver, VirtualServoDriver 6 | 7 | /** 8 | * @class ServoModel 9 | * this class defines the parameters of a certain model servo 10 | **/ 11 | export class ServoModel { 12 | /** 13 | * Construct a new ServoModel 14 | * @param {Number} frequency the frequency on which the servo operates in Hz, defaults to 50 15 | * @param {Number} fullLeftDutyCycle the dutyCycle in percentage which may be used to move the servo to it's full left position 16 | * @param {Number} neutralDutyCycle the dutyCycle in percentage which may be used to move the servo to it's neutral position 17 | * @param {Number} fullRightDutyCycle the dutyCycle in percentage which may be used to move the servo to it's full right position 18 | * @param {Number} rangeDegrees the number of degrees the servo can rotate, defaults to 180 19 | * @param {Number} maxRangeMotionSeconds the number of seconds it takes the servo to move from full left to full right position, this is used to calculate when motion is actually done 20 | **/ 21 | constructor(frequency = 50, fullLeftDutyCycle = 5, neutralDutyCycle = 7.5, fullRightDutyCycle = 10, rangeDegrees = 180, maxRangeMotionSeconds = 0.35) { 22 | 23 | assert.is_number(frequency, "frequency should be a positive Number between 0 and 2000"); 24 | assert.is_number(fullLeftDutyCycle, "fullLeftDutyCycle should be a Number between 0 and 100"); 25 | assert.is_number(neutralDutyCycle, "neutralDutyCycle should be a Number between 0 and 100"); 26 | assert.is_number(fullRightDutyCycle, "neutralDutyCycle should be a Number between 0 and 100"); 27 | assert.is_number(rangeDegrees, "rangeDegrees should be a Number between 0 and 360"); 28 | assert.is_number(maxRangeMotionSeconds, "maxRangeMotionSeconds should be a Number between 0 and 10"); 29 | 30 | assert.is_lt(frequency, 2001, "frequency should be a Number between 0 and 2000"); 31 | assert.is_gt(frequency, -1, "frequency should be a Number between 0 and 2000"); 32 | 33 | assert.is_gt(neutralDutyCycle, -1, "neutralDutyCycle should be a Number between 0 and 100"); 34 | assert.is_lt(neutralDutyCycle, 101, "neutralDutyCycle should be a Number between 0 and 100"); 35 | 36 | assert.is_gt(fullLeftDutyCycle, -1, "fullLeftDutyCycle should be a Number between 0 and 100"); 37 | assert.is_lt(fullLeftDutyCycle, neutralDutyCycle, "fullLeftDutyCycle should be a lower Number than neutralDutyCycle"); 38 | 39 | assert.is_gt(fullRightDutyCycle, neutralDutyCycle, "fullRightDutyCycle should be a greater Number than neutralDutyCycle"); 40 | assert.is_lt(fullRightDutyCycle, 101, "fullRightDutyCycle should be a Number between 0 and 100"); 41 | 42 | assert.is_lt(rangeDegrees, 361, "rangeDegrees should be a Number between 0 and 360"); 43 | assert.is_gt(rangeDegrees, -1, "rangeDegrees should be a Number between 0 and 360"); 44 | 45 | assert.is_lt(maxRangeMotionSeconds, 10, "maxRangeMotionSeconds should be a Number between 0 and 10"); 46 | assert.is_gt(maxRangeMotionSeconds, 0, "maxRangeMotionSeconds should be a Number between 0 and 10"); 47 | 48 | this.frequency = frequency; 49 | this.fullLeftDutyCycle = fullLeftDutyCycle; 50 | this.neutralDutyCycle = neutralDutyCycle; 51 | this.fullRightDutyCycle = fullRightDutyCycle; 52 | this.rangeDegrees = rangeDegrees; 53 | this.maxRangeMotionSeconds = maxRangeMotionSeconds; 54 | 55 | } 56 | } 57 | 58 | export const MG90SServo = new ServoModel(50, 2, 7, 12, 180, 0.35); 59 | export const MG995Servo = new ServoModel(50, 2, 7, 12, 180, 0.8); 60 | 61 | export class ServoDriver { 62 | async init() { 63 | throw Error("unimplemented"); 64 | } 65 | 66 | async left() { 67 | throw Error("unimplemented"); 68 | } 69 | 70 | async right() { 71 | throw Error("unimplemented"); 72 | } 73 | 74 | async neutral() { 75 | throw Error("unimplemented"); 76 | } 77 | 78 | /** 79 | * move the servo to a certain angle 80 | * @param {Number} degrees where neutral = 0 and left is a negative number and right is a positive number 81 | */ 82 | async angle(degrees = 0) { 83 | throw Error("unimplemented"); 84 | } 85 | 86 | async off() { 87 | throw Error("unimplemented"); 88 | } 89 | } 90 | 91 | export class SoftPwmDriver extends ServoDriver { 92 | 93 | /** 94 | * init a new SoftPwmDriver 95 | * @param {String} chip name of the gpiochip, defaults to /dev/gpiochip0 96 | * @param {Number} pinNum 97 | * @param {ServoModel} servoModel 98 | */ 99 | constructor(chip = '/dev/gpiochip0', pinNum = 18, servoModel) { 100 | super(); 101 | assert.is_string(chip, "chip should be a String like \"/dev/gpiochip0\""); 102 | assert.is_number(pinNum, "pinNum should be a positive Number"); 103 | assert.is_gt(pinNum, -1, "pinNum should be a positive Number"); 104 | 105 | assert.is_instance_of(servoModel, ServoModel, "servoModel should be an instance of ServoModel"); 106 | 107 | this.servoModel = servoModel; 108 | 109 | this.chip = chip; 110 | this.pinNum = pinNum; 111 | 112 | this.pinSet = new PinSet(); 113 | 114 | } 115 | 116 | async init() { 117 | await this.pinSet.init(this.chip, 'out', [this.pinNum]); 118 | } 119 | 120 | softPwm(frequency, dutyCycle) { 121 | console.trace("softPwm %s, %s", frequency, dutyCycle); 122 | this.pinSet.softPwm(frequency, dutyCycle); 123 | // todo calc 124 | let time = this.servoModel.maxRangeMotionSeconds * 1000; 125 | return new Promise((resolve, reject) => { 126 | setTimeout(resolve, time); 127 | }); 128 | } 129 | 130 | async left() { 131 | await this.softPwm(this.servoModel.frequency, this.servoModel.fullLeftDutyCycle); 132 | } 133 | 134 | async right() { 135 | await this.softPwm(this.servoModel.frequency, this.servoModel.fullRightDutyCycle); 136 | } 137 | 138 | async neutral() { 139 | await this.softPwm(this.servoModel.frequency, this.servoModel.neutralDutyCycle); 140 | } 141 | 142 | /** 143 | * move the servo to a certain angle 144 | * @param {Number} degrees where neutral = 0 and left is a negative number and right is a positive number 145 | */ 146 | async angle(degrees = 0) { 147 | throw Error("unimplemented"); 148 | } 149 | 150 | async off(){ 151 | await this.pinSet.softPwmOff(); 152 | } 153 | } 154 | 155 | /** 156 | * @class Servo 157 | * @description represents an abstract Servo motor which can be positioned 158 | **/ 159 | export class Servo { 160 | 161 | /** 162 | * @constructor 163 | * @param {ServoDriver} driver the driver to use for this Servo 164 | */ 165 | constructor(driver) { 166 | assert.is_instance_of(driver, ServoDriver, "driver should be an instance of ServoDriver"); 167 | this.driver = driver; 168 | } 169 | 170 | /** 171 | * init the servo (or most of the time, it's driver) 172 | */ 173 | async init() { 174 | await this.driver.init(); 175 | } 176 | 177 | /** 178 | * position the Servo in its most left position 179 | */ 180 | async left() { 181 | await this.driver.left(); 182 | } 183 | 184 | /** 185 | * position the Servo in its most right position 186 | */ 187 | async right() { 188 | await this.driver.right(); 189 | } 190 | 191 | /** 192 | * position the Servo in its neutral position 193 | */ 194 | async neutral() { 195 | await this.driver.neutral(); 196 | } 197 | 198 | /** 199 | * move the servo to a certain angle 200 | * @param {Number} degrees where neutral = 0 and left is a negative number and right is a positive number 201 | */ 202 | async angle(degrees = 0) { 203 | await this.driver.angle(degrees); 204 | } 205 | 206 | /** 207 | * turn of the pwm signal (this will NOT reposition the servo to its neutral position) 208 | */ 209 | async off(){ 210 | await this.driver.off(); 211 | } 212 | } -------------------------------------------------------------------------------- /src/modules/encoding/mod.rs: -------------------------------------------------------------------------------- 1 | use base64::Engine; 2 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 3 | use quickjs_runtime::jsutils::jsproxies::JsProxy; 4 | use quickjs_runtime::jsutils::modules::NativeModuleLoader; 5 | use quickjs_runtime::jsutils::JsError; 6 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 7 | use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 8 | 9 | struct EncodingModuleLoader {} 10 | 11 | impl NativeModuleLoader for EncodingModuleLoader { 12 | fn has_module(&self, _realm: &QuickJsRealmAdapter, module_name: &str) -> bool { 13 | module_name.eq("greco://encoding") 14 | } 15 | 16 | fn get_module_export_names( 17 | &self, 18 | _realm: &QuickJsRealmAdapter, 19 | _module_name: &str, 20 | ) -> Vec<&str> { 21 | vec!["Base64"] 22 | } 23 | 24 | fn get_module_exports( 25 | &self, 26 | realm: &QuickJsRealmAdapter, 27 | _module_name: &str, 28 | ) -> Vec<(&str, QuickJsValueAdapter)> { 29 | init_exports(realm).expect("init encoding exports failed") 30 | } 31 | } 32 | 33 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 34 | builder.native_module_loader(EncodingModuleLoader {}) 35 | } 36 | 37 | fn init_exports( 38 | realm: &QuickJsRealmAdapter, 39 | ) -> Result, JsError> { 40 | let base64_proxy_class = create_base64_proxy(realm); 41 | let base64_proxy_class_res = realm.install_proxy(base64_proxy_class, false)?; 42 | 43 | Ok(vec![("Base64", base64_proxy_class_res)]) 44 | } 45 | 46 | pub(crate) fn create_base64_proxy(_realm: &QuickJsRealmAdapter) -> JsProxy { 47 | JsProxy::new() 48 | .namespace(&["greco", "encoding"]) 49 | .name("Base64") 50 | .static_method("encode", |_runtime, realm, args| { 51 | // todo async 52 | 53 | if args.is_empty() { 54 | Err(JsError::new_str( 55 | "encode expects a single type array or string arg", 56 | )) 57 | } else if args[0].is_typed_array() { 58 | let bytes = realm.copy_typed_array_buffer(&args[0])?; 59 | let engine = base64::engine::general_purpose::STANDARD; 60 | let encoded = engine.encode(bytes); 61 | 62 | realm.create_string(encoded.as_str()) 63 | } else if args[0].is_string() { 64 | let engine = base64::engine::general_purpose::STANDARD; 65 | let string = args[0].to_string()?; 66 | let encoded = engine.encode(&string); 67 | 68 | realm.create_string(encoded.as_str()) 69 | } else { 70 | Err(JsError::new_str( 71 | "encode expects a single type array or string arg", 72 | )) 73 | } 74 | }) 75 | .static_method("encodeSync", |_runtime, realm, args| { 76 | if args.is_empty() { 77 | Err(JsError::new_str( 78 | "encodeSync expects a single type array or string arg", 79 | )) 80 | } else if args[0].is_typed_array() { 81 | let bytes = realm.copy_typed_array_buffer(&args[0])?; 82 | let engine = base64::engine::general_purpose::STANDARD; 83 | let encoded = engine.encode(bytes); 84 | 85 | realm.create_string(encoded.as_str()) 86 | } else if args[0].is_string() { 87 | let engine = base64::engine::general_purpose::STANDARD; 88 | let string = args[0].to_string()?; 89 | let encoded = engine.encode(&string); 90 | 91 | realm.create_string(encoded.as_str()) 92 | } else { 93 | Err(JsError::new_str( 94 | "encodeSync expects a single type array or string arg", 95 | )) 96 | } 97 | }) 98 | .static_method("decode", |_runtime, realm, args| { 99 | // todo async 100 | 101 | if args.is_empty() || !args[0].is_string() { 102 | Err(JsError::new_str("decode expects a single string arg")) 103 | } else { 104 | let s = args[0].to_string()?; 105 | realm.create_resolving_promise( 106 | move || { 107 | let engine = base64::engine::general_purpose::STANDARD_NO_PAD; 108 | let decoded = engine.decode(s.trim_end_matches('=')).map_err(|e| { 109 | JsError::new_string(format!("could not decode base64({s}): {e}")) 110 | })?; 111 | Ok(decoded) 112 | }, 113 | |realm, p_res| { 114 | // 115 | realm.create_typed_array_uint8(p_res) 116 | }, 117 | ) 118 | } 119 | }) 120 | .static_method("decodeString", |_runtime, realm, args| { 121 | // todo async 122 | 123 | if args.is_empty() || !args[0].is_string() { 124 | Err(JsError::new_str("decodeString expects a single string arg")) 125 | } else { 126 | let s = args[0].to_string()?; 127 | realm.create_resolving_promise( 128 | move || { 129 | let engine = base64::engine::general_purpose::STANDARD_NO_PAD; 130 | let decoded = engine.decode(s.trim_end_matches('=')).map_err(|e| { 131 | JsError::new_string(format!("could not decode base64({s}): {e}")) 132 | })?; 133 | let s = String::from_utf8_lossy(&decoded); 134 | Ok(s.to_string()) 135 | }, 136 | |realm, p_res| { 137 | // 138 | realm.create_string(p_res.as_str()) 139 | }, 140 | ) 141 | } 142 | }) 143 | .static_method("decodeStringSync", |_runtime, realm, args| { 144 | if args.is_empty() || !args[0].is_string() { 145 | Err(JsError::new_str("decodeString expects a single string arg")) 146 | } else { 147 | let s = args[0].to_string()?; 148 | realm.create_resolving_promise( 149 | move || { 150 | let engine = base64::engine::general_purpose::STANDARD_NO_PAD; 151 | let decoded = engine.decode(s.trim_end_matches('=')).map_err(|e| { 152 | JsError::new_string(format!("could not decode base64({s}): {e}")) 153 | })?; 154 | let s = String::from_utf8_lossy(&decoded); 155 | Ok(s.to_string()) 156 | }, 157 | |realm, p_res| { 158 | // 159 | realm.create_string(p_res.as_str()) 160 | }, 161 | ) 162 | } 163 | }) 164 | .static_method("decodeSync", |_runtime, realm, args| { 165 | // todo async 166 | 167 | if args.is_empty() || !args[0].is_string() { 168 | Err(JsError::new_str("decode expects a single string arg")) 169 | } else { 170 | let s = args[0].to_string()?; 171 | let engine = base64::engine::general_purpose::STANDARD_NO_PAD; 172 | let decoded = engine.decode(s.trim_end_matches('=')).map_err(|e| { 173 | JsError::new_string(format!("could not decode base64({s}): {e}")) 174 | })?; 175 | // 176 | realm.create_typed_array_uint8(decoded) 177 | } 178 | }) 179 | } 180 | 181 | #[cfg(test)] 182 | pub mod tests { 183 | use futures::executor::block_on; 184 | //use log::LevelFilter; 185 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 186 | use quickjs_runtime::jsutils::Script; 187 | use quickjs_runtime::values::JsValueFacade; 188 | 189 | #[test] 190 | fn test_b64() { 191 | //simple_logging::log_to_stderr(log::LevelFilter::Info); 192 | 193 | let builder = QuickJsRuntimeBuilder::new(); 194 | let builder = crate::init_greco_rt(builder); 195 | let rt = builder.build(); 196 | 197 | let script = Script::new( 198 | "test_encoding.js", 199 | r#" 200 | 201 | async function test() { 202 | let encodingMod = await import('greco://encoding'); 203 | 204 | let data = 'QUJDRA=='; 205 | //Uint8Array(4) [ 65, 66, 67, 68 ] 206 | 207 | let arr = await encodingMod.Base64.decode(data); 208 | let b64 = await encodingMod.Base64.encode(arr); 209 | arr = encodingMod.Base64.decodeSync(b64); 210 | b64 = encodingMod.Base64.encodeSync(arr); 211 | 212 | return b64; 213 | 214 | } 215 | 216 | test() 217 | 218 | "#, 219 | ); 220 | let res: JsValueFacade = block_on(rt.eval(None, script)).ok().expect("script failed"); 221 | 222 | println!("{}", res.stringify()); 223 | if let JsValueFacade::JsPromise { cached_promise } = res { 224 | let p_res = block_on(cached_promise.get_promise_result()) 225 | .ok() 226 | .expect("get prom res failed"); 227 | match p_res { 228 | Ok(jsvf) => { 229 | println!("prom resolved to {}", jsvf.stringify()); 230 | } 231 | Err(e) => { 232 | panic!("prom rejected: {}", e.stringify()); 233 | } 234 | } 235 | } else { 236 | panic!("did not get a promise"); 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/modules/io/gpio/pinset.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::StreamExt; 2 | use gpio_cdev::{ 3 | AsyncLineEventHandle, Chip, EventRequestFlags, Line, LineEvent, LineHandle, LineRequestFlags, 4 | }; 5 | use hirofa_utils::eventloop::EventLoop; 6 | use hirofa_utils::task_manager::TaskManager; 7 | use std::cell::RefCell; 8 | use std::future::Future; 9 | use std::ops::{Div, Sub}; 10 | use std::sync::Arc; 11 | use std::time::{Duration, Instant}; 12 | 13 | thread_local! { 14 | static PIN_SET: RefCell = RefCell::new(PinSet::new()); 15 | } 16 | 17 | pub struct PinSetHandle { 18 | event_loop: EventLoop, 19 | // this indicator is passed to the worker thread and may be altered to modify the pwm signal 20 | pub pwm_stop_sender: Option>, 21 | } 22 | 23 | impl PinSetHandle { 24 | pub fn new() -> Self { 25 | Self { 26 | event_loop: EventLoop::new(), 27 | pwm_stop_sender: None, 28 | } 29 | } 30 | pub fn do_with R + Send + 'static>( 31 | &self, 32 | consumer: C, 33 | ) -> impl Future { 34 | self.event_loop.add(|| { 35 | PIN_SET.with(|rc| { 36 | let ps = &*rc.borrow(); 37 | consumer(ps) 38 | }) 39 | }) 40 | } 41 | pub fn do_with_void(&self, consumer: C) { 42 | self.event_loop.add_void(|| { 43 | PIN_SET.with(|rc| { 44 | let ps = &*rc.borrow(); 45 | consumer(ps) 46 | }) 47 | }) 48 | } 49 | 50 | pub fn do_with_mut R + Send + 'static>( 51 | &self, 52 | consumer: C, 53 | ) -> impl Future { 54 | self.event_loop.add(|| { 55 | PIN_SET.with(|rc| { 56 | let ps = &mut *rc.borrow_mut(); 57 | consumer(ps) 58 | }) 59 | }) 60 | } 61 | } 62 | 63 | pub struct PinSet { 64 | output_handles: Vec, 65 | lines: Vec, 66 | input_task_manager: TaskManager, 67 | } 68 | 69 | #[derive(Clone, Copy)] 70 | pub enum PinMode { 71 | In, 72 | Out, 73 | } 74 | 75 | #[allow(dead_code)] 76 | impl PinSet { 77 | pub fn new() -> Self { 78 | Self { 79 | output_handles: vec![], 80 | lines: vec![], 81 | input_task_manager: TaskManager::new(2), 82 | } 83 | } 84 | pub fn set_event_handler(&mut self, handler: H) -> Result<(), String> 85 | where 86 | H: Fn(u32, LineEvent) + Sync + Send + 'static, 87 | { 88 | log::debug!("init PinSet evt handler"); 89 | // self.event_handler = Some(Arc::new(handler)); 90 | // start listener, for every pin? 91 | // stop current listener? 92 | let handler_arc = Arc::new(handler); 93 | 94 | for line in &self.lines { 95 | let pin = line.offset(); 96 | 97 | let event_handle = line 98 | .events( 99 | LineRequestFlags::INPUT, 100 | EventRequestFlags::BOTH_EDGES, 101 | "PinSet_read-input", 102 | ) 103 | .map_err(|e| format!("{e}"))?; 104 | 105 | let handler_arc = handler_arc.clone(); 106 | 107 | let _ignore = self.input_task_manager.add_task_async(async move { 108 | log::trace!("PinSet running async helper"); 109 | let async_event_handle_res = 110 | AsyncLineEventHandle::new(event_handle).map_err(|e| format!("{e}")); 111 | let mut async_event_handle = match async_event_handle_res { 112 | Ok(handle) => handle, 113 | Err(e) => { 114 | log::error!("AsyncLineEventHandle init failed: {}", e.as_str()); 115 | panic!("AsyncLineEventHandle init failed: {}", e.as_str()); 116 | } 117 | }; 118 | while let Some(evt) = async_event_handle.next().await { 119 | let evt_res = evt.map_err(|e| format!("{e}")); 120 | match evt_res { 121 | Ok(evt) => { 122 | log::trace!("GPIO Event @{} : {:?}", pin, evt); 123 | handler_arc(pin, evt); 124 | } 125 | Err(e) => { 126 | log::trace!("GPIO Err: {:?}", e); 127 | } 128 | } 129 | } 130 | log::info!("end async while"); 131 | }); 132 | } 133 | Ok(()) 134 | } 135 | pub fn init(&mut self, chip_name: &str, mode: PinMode, pins: &[u32]) -> Result<(), String> { 136 | log::debug!( 137 | "PinSet.init c:{} m:{:?} p:{}", 138 | chip_name, 139 | match mode { 140 | PinMode::In => { 141 | "in" 142 | } 143 | PinMode::Out => { 144 | "out" 145 | } 146 | }, 147 | pins.len() 148 | ); 149 | // chip_name = "/dev/gpiochip0" 150 | let mut chip = Chip::new(chip_name).map_err(|e| format!("{e}"))?; 151 | 152 | for x in pins { 153 | let line = chip.get_line(*x).map_err(|e| format!("{e}"))?; 154 | 155 | match mode { 156 | PinMode::In => { 157 | self.lines.push(line); 158 | } 159 | PinMode::Out => { 160 | let handle = line 161 | .request(LineRequestFlags::OUTPUT, 0, "PinSet_set-output") 162 | .map_err(|e| format!("{e}"))?; 163 | self.output_handles.push(handle) 164 | } 165 | }; 166 | } 167 | Ok(()) 168 | } 169 | pub fn set_state(&self, states: &[u8]) -> Result<(), String> { 170 | log::trace!("PinSet.set_state: len:{}", states.len()); 171 | for (x, state) in states.iter().enumerate() { 172 | self.set_state_index(x, *state)?; 173 | } 174 | Ok(()) 175 | } 176 | pub fn set_state_index(&self, pin_idx: usize, state: u8) -> Result<(), String> { 177 | //log::trace!("PinSet.set_state_index: idx: {}, state: {}", pin_idx, state); 178 | 179 | let handle = &self.output_handles[pin_idx]; 180 | handle.set_value(state).map_err(|e| format!("{e}"))?; 181 | 182 | Ok(()) 183 | } 184 | pub fn get_state(&self) -> Result, String> { 185 | let mut ret = vec![]; 186 | for handle in &self.output_handles { 187 | ret.push(handle.get_value().map_err(|ex| format!("{ex}"))?); 188 | } 189 | Ok(ret) 190 | } 191 | pub fn get_state_index(&self, index: usize) -> Result { 192 | self.output_handles[index] 193 | .get_value() 194 | .map_err(|ex| format!("{ex}")) 195 | } 196 | pub fn sequence( 197 | &self, 198 | steps: Vec>, 199 | pause_between_steps_ms: i32, 200 | repeats: i32, 201 | ) -> Result<(), String> { 202 | let sleep = Duration::from_millis(pause_between_steps_ms as u64); 203 | 204 | for _ in 0..repeats { 205 | for step in &steps { 206 | self.set_state(step.as_slice())?; 207 | std::thread::sleep(sleep); 208 | } 209 | } 210 | 211 | Ok(()) 212 | } 213 | 214 | pub fn run_pwm_sequence( 215 | &self, 216 | frequency: u64, 217 | duty_cycle: f64, 218 | pulse_count: usize, 219 | pwm_stop_receiver: std::sync::mpsc::Receiver, 220 | ) -> Result<(), String> { 221 | let period = Duration::from_micros(1000000u64 / frequency); 222 | let on_time = period.div_f64(100f64 / duty_cycle); 223 | let off_time = period.sub(on_time); 224 | 225 | log::trace!( 226 | "run_pwm_sequence period = {:?}, on_time = {:?} , off_time={:?}", 227 | period, 228 | on_time, 229 | off_time 230 | ); 231 | 232 | let start = Instant::now(); 233 | 234 | let mut ct = 0; 235 | while pulse_count == 0 || ct < pulse_count { 236 | if pwm_stop_receiver.try_recv().is_ok() { 237 | break; 238 | } else { 239 | std::thread::sleep(off_time); 240 | 241 | if let Some(err) = self.set_state_index(0, 1).err() { 242 | return Err(format!("An error occurred in the pwm sequence: {err}")); 243 | } 244 | std::thread::sleep(on_time); 245 | if let Some(err) = self.set_state_index(0, 0).err() { 246 | return Err(format!("An error occurred in the pwm sequence: {err}")); 247 | } 248 | } 249 | 250 | ct += 1; 251 | } 252 | 253 | let end = Instant::now(); 254 | let total_time = end.duration_since(start); 255 | let pulse_time = total_time.div(ct as u32); 256 | let hz = 1000 / pulse_time.as_millis(); 257 | log::debug!( 258 | "run_pwm_sequence ended pulse_ct={} total_time={:?} pulse_time={:?} freq={}hz", 259 | ct, 260 | total_time, 261 | pulse_time, 262 | hz 263 | ); 264 | 265 | Ok(()) 266 | } 267 | 268 | pub fn start_pwm_sequence( 269 | &self, 270 | frequency: u64, 271 | duty_cycle: f64, 272 | pwm_stop_receiver: std::sync::mpsc::Receiver, 273 | ) { 274 | let period = Duration::from_micros(1000000u64 / frequency); 275 | let on_time = period.div_f64(100f64 / duty_cycle); 276 | let off_time = period.sub(on_time); 277 | 278 | log::trace!( 279 | "start_pwm_sequence period = {:?}, on_time = {:?} , off_time={:?}", 280 | period, 281 | on_time, 282 | off_time 283 | ); 284 | 285 | loop { 286 | if pwm_stop_receiver.try_recv().is_ok() { 287 | break; 288 | } else { 289 | std::thread::sleep(off_time); 290 | 291 | if let Some(err) = self.set_state_index(0, 1).err() { 292 | log::error!("An error occurred in the pwm sequence: {}", err); 293 | break; 294 | } 295 | std::thread::sleep(on_time); 296 | if let Some(err) = self.set_state_index(0, 0).err() { 297 | log::error!("An error occurred in the pwm sequence: {}", err); 298 | break; 299 | } 300 | } 301 | } 302 | } 303 | } 304 | 305 | impl Drop for PinSet { 306 | fn drop(&mut self) { 307 | log::trace!("Drop.drop for PinSet"); 308 | } 309 | } 310 | 311 | impl Default for PinSet { 312 | fn default() -> Self { 313 | Self::new() 314 | } 315 | } 316 | 317 | impl Default for PinSetHandle { 318 | fn default() -> Self { 319 | Self::new() 320 | } 321 | } 322 | 323 | impl Drop for PinSetHandle { 324 | fn drop(&mut self) { 325 | log::trace!("Drop.drop for PinSetHandle"); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/modules/jwt/mod.rs: -------------------------------------------------------------------------------- 1 | //! # JWT 2 | //! this module adds support for JWT 3 | //! 4 | //! # Example 5 | //! ```javascript 6 | //! async function test() { 7 | //! const alg = "EdDSA"; // or RS512 8 | //! 9 | //! const jwtMod = await import("greco://jwt"); 10 | //! const key = await jwtMod.generateKey(alg); 11 | //! 12 | //! const payload = {'user': 'somebody', 'obj': 'abcdef', 'privs': ['write', 'read']}; 13 | //! const headers = { alg, typ: "JWT" }; 14 | //! 15 | //! const jwtToken = await jwtMod.create(headers, payload, key); 16 | //! 17 | //! const validatedPayload = await jwtMod.verify(jwtToken, key, alg); 18 | //! // validatedPayload will be like {"iat":1646137320,"exp":1646223720,"nbf":1646137320,"jti":"3ad1275f-e577-452e-a48f-413b6463b869", "user": "somebody", "obj": "abcdef", "privs": ["write", "read"]} 19 | //! return(jwtToken + " -> " + JSON.stringify(validatedPayload)); 20 | //! 21 | //! }; 22 | //! ``` 23 | //! 24 | 25 | use crate::modules::jwt::JwtAlgo::{EdDSA, RS512}; 26 | use jwt_simple::prelude::*; 27 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 28 | use quickjs_runtime::jsutils::modules::NativeModuleLoader; 29 | use quickjs_runtime::jsutils::JsError; 30 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 31 | use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 32 | use quickjs_runtime::values::JsValueFacade::TypedArray; 33 | use quickjs_runtime::values::{JsValueFacade, TypedArrayType}; 34 | use serde_json::Value; 35 | use std::fmt::Display; 36 | use std::str::FromStr; 37 | 38 | struct JwtModuleLoader {} 39 | 40 | impl NativeModuleLoader for JwtModuleLoader { 41 | fn has_module(&self, _realm: &QuickJsRealmAdapter, module_name: &str) -> bool { 42 | module_name.eq("greco://jwt") 43 | } 44 | 45 | fn get_module_export_names( 46 | &self, 47 | _realm: &QuickJsRealmAdapter, 48 | _module_name: &str, 49 | ) -> Vec<&str> { 50 | vec!["create", "verify", "generateKey"] 51 | } 52 | 53 | fn get_module_exports( 54 | &self, 55 | realm: &QuickJsRealmAdapter, 56 | _module_name: &str, 57 | ) -> Vec<(&str, QuickJsValueAdapter)> { 58 | init_exports(realm).expect("init jwt exports failed") 59 | } 60 | } 61 | 62 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 63 | builder.native_module_loader(JwtModuleLoader {}) 64 | } 65 | 66 | fn init_exports( 67 | realm: &QuickJsRealmAdapter, 68 | ) -> Result, JsError> { 69 | let create = realm.create_function("create", create, 3)?; 70 | let verify = realm.create_function_async("verify", verify, 3)?; 71 | let generate_key = realm.create_function_async("generateKey", generate_key, 1)?; 72 | 73 | Ok(vec![ 74 | ("create", create), 75 | ("verify", verify), 76 | ("generateKey", generate_key), 77 | ]) 78 | } 79 | 80 | pub enum JwtAlgo { 81 | EdDSA, 82 | RS512, 83 | } 84 | 85 | impl FromStr for JwtAlgo { 86 | type Err = JsError; 87 | 88 | fn from_str(s: &str) -> Result { 89 | if s.eq_ignore_ascii_case("rs512") { 90 | Ok(RS512) 91 | } else if s.eq_ignore_ascii_case("eddsa") { 92 | Ok(EdDSA) 93 | } else { 94 | Err(JsError::new_str("Unsupported algoritm")) 95 | } 96 | } 97 | } 98 | 99 | impl Display for JwtAlgo { 100 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 101 | let str = match self { 102 | EdDSA => "EdDSA".to_string(), 103 | RS512 => "Rs512".to_string(), 104 | }; 105 | write!(f, "{}", str) 106 | } 107 | } 108 | 109 | /// create a new JWT token 110 | /// 3 args 111 | /// 0: Object headers 112 | /// 1: Object payload 113 | /// 2: TypedArray key 114 | fn create( 115 | realm: &QuickJsRealmAdapter, 116 | _this: &QuickJsValueAdapter, 117 | args: &[QuickJsValueAdapter], 118 | ) -> Result { 119 | if args.len() != 3 || !args[0].is_object() || !args[1].is_object() || !args[2].is_typed_array() 120 | { 121 | Err(JsError::new_str("invalid arguments for create")) 122 | } else { 123 | let alg_header = realm.get_object_property(&args[0], "alg")?; 124 | let alg = if alg_header.is_string() { 125 | let string = alg_header.to_string()?; 126 | JwtAlgo::from_str(string.as_str())? 127 | } else { 128 | JwtAlgo::EdDSA 129 | }; 130 | 131 | let payload_json = realm.json_stringify(&args[1], None)?; 132 | 133 | // todo create utils so we can borrow the buffer (with_buffer?) 134 | let key_bytes = realm.copy_typed_array_buffer(&args[2])?; 135 | 136 | realm.create_resolving_promise( 137 | move || { 138 | let custom: Value = serde_json::from_str(payload_json.as_str()).map_err(|er| { 139 | JsError::new_string(format!("could not parse json payload {er}")) 140 | })?; 141 | 142 | // todo parse duration from headers? 143 | let claims = Claims::with_custom_claims(custom, Duration::from_days(1)) 144 | .with_jwt_id(uuid::Uuid::new_v4()); 145 | 146 | let token = match alg { 147 | EdDSA => { 148 | let key = 149 | Ed25519KeyPair::from_bytes(key_bytes.as_slice()).map_err(|err| { 150 | JsError::new_string(format!( 151 | "could not create key from bytes {err}" 152 | )) 153 | })?; 154 | key.sign(claims) 155 | .map_err(|err| JsError::new_string(format!("{err}")))? 156 | } 157 | RS512 => { 158 | let key = RS512KeyPair::from_der(key_bytes.as_slice()).map_err(|err| { 159 | JsError::new_string(format!("could not create key from bytes {err}")) 160 | })?; 161 | key.sign(claims) 162 | .map_err(|err| JsError::new_string(format!("{err}")))? 163 | } 164 | }; 165 | 166 | Ok(token) 167 | }, 168 | |realm, res| realm.create_string(res.as_str()), 169 | ) 170 | } 171 | } 172 | 173 | /// verify a token and return payload 174 | /// 3 args 175 | /// 0: String token 176 | /// 1: TypedArray key 177 | /// 2: String algorithm 178 | async fn verify(_this: JsValueFacade, args: Vec) -> Result { 179 | if !args.len() == 3 || !args[0].is_string() || !args[2].is_string() { 180 | Err(JsError::new_str("invalid args for verify")) 181 | } else if let TypedArray { 182 | buffer: key_bytes, 183 | array_type: _, 184 | } = &args[1] 185 | { 186 | let token = args[0].get_str(); 187 | let alg = JwtAlgo::from_str(args[2].get_str())?; 188 | 189 | let parsed_claims = match alg { 190 | EdDSA => { 191 | let key = Ed25519KeyPair::from_bytes(key_bytes.as_slice()).map_err(|err| { 192 | JsError::new_string(format!("could not create key from bytes {err}")) 193 | })?; 194 | key.public_key() 195 | .verify_token::(token, None) 196 | .map_err(|err| JsError::new_string(format!("{err}")))? 197 | } 198 | RS512 => { 199 | let key = RS512KeyPair::from_der(key_bytes.as_slice()).map_err(|err| { 200 | JsError::new_string(format!("could not create key from bytes {err}")) 201 | })?; 202 | key.public_key() 203 | .verify_token::(token, None) 204 | .map_err(|err| JsError::new_string(format!("could not verify token{err}")))? 205 | } 206 | }; 207 | 208 | let payload_json = serde_json::to_string(&parsed_claims) 209 | .map_err(|err| JsError::new_string(format!("could not serialize claims {err}")))?; 210 | 211 | Ok(JsValueFacade::JsonStr { json: payload_json }) 212 | } else { 213 | Err(JsError::new_str("invalid args for verify")) 214 | } 215 | } 216 | 217 | /// generate a new key and return as typedarray 218 | /// 1 arg, for key type RS512 or EdDSA 219 | async fn generate_key( 220 | _this: JsValueFacade, 221 | args: Vec, 222 | ) -> Result { 223 | let key_bytes = if !args.is_empty() && args[0].is_string() { 224 | let alg = JwtAlgo::from_str(args[0].get_str())?; 225 | match alg { 226 | RS512 => Ok::, JsError>( 227 | RS512KeyPair::generate(4096) 228 | .map_err(|err| { 229 | JsError::new_string(format!("could not create RS512 keypair {err}")) 230 | })? 231 | .to_der() 232 | .map_err(|err| { 233 | JsError::new_string(format!("could not create RS512 keypair2 {err}")) 234 | })?, 235 | ), 236 | EdDSA => Ok(Ed25519KeyPair::generate().to_bytes()), 237 | }? 238 | } else { 239 | Ed25519KeyPair::generate().to_bytes() 240 | }; 241 | 242 | let res = JsValueFacade::TypedArray { 243 | buffer: key_bytes, 244 | array_type: TypedArrayType::Uint8, 245 | }; 246 | Ok(res) 247 | } 248 | 249 | #[cfg(test)] 250 | pub mod tests { 251 | use crate::init_greco_rt; 252 | use futures::executor::block_on; 253 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 254 | use quickjs_runtime::jsutils::Script; 255 | use quickjs_runtime::values::JsValueFacade; 256 | 257 | #[test] 258 | fn test_uuid() { 259 | let rt = init_greco_rt(QuickJsRuntimeBuilder::new()).build(); 260 | let script = Script::new( 261 | "uuid.js", 262 | r#" 263 | async function test() { 264 | 265 | const alg = "EdDSA"; 266 | 267 | const jwtMod = await import("greco://jwt"); 268 | const key = await jwtMod.generateKey(alg); 269 | 270 | const payload = {'user': 'somebody', 'obj': 'abcdef', 'privs': ['write', 'read']}; 271 | const headers = { alg, typ: "JWT" }; 272 | 273 | const jwtToken = await jwtMod.create(headers, payload, key); 274 | 275 | // 276 | 277 | const validatedPayload = await jwtMod.verify(jwtToken, key, alg); 278 | 279 | return(jwtToken + " -> " + JSON.stringify(validatedPayload)); 280 | 281 | }; 282 | test(); 283 | "#, 284 | ); 285 | let res = block_on(rt.eval(None, script)).ok().expect("script failed"); 286 | 287 | if let JsValueFacade::JsPromise { cached_promise } = res { 288 | let prom_res = block_on(cached_promise.get_promise_result()) 289 | .ok() 290 | .expect("promise timed out"); 291 | 292 | match prom_res { 293 | Ok(res) => { 294 | let s = res.get_str(); 295 | println!("jwt test res was {s}"); 296 | } 297 | Err(err) => { 298 | panic!("prmise was rejected {}", err.stringify()); 299 | } 300 | } 301 | } else { 302 | panic!("not a promise"); 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/modules/io/fs/mod.rs: -------------------------------------------------------------------------------- 1 | //! #FS Module 2 | //! 3 | //! ```javascript 4 | //! async function test() { 5 | //! let fs_mod = await import('greco://fs'); 6 | //! await fs_mod.write('./test.txt', 'hello from greco fs'); 7 | //! } 8 | //! ``` 9 | //! # example 10 | //! 11 | //! ```rust 12 | //! use quickjs_runtime::builder::QuickJsRuntimeBuilder; 13 | //! use futures::executor::block_on; 14 | //! use quickjs_runtime::jsutils::Script; 15 | //! use quickjs_runtime::values::JsValueFacade; 16 | //! let rtb = QuickJsRuntimeBuilder::new(); 17 | //! let rtb = green_copper_runtime::init_greco_rt(rtb); 18 | //! let rt = rtb.build(); 19 | //! rt.eval_sync(None, Script::new("init_fs.es", "async function test_write() {\ 20 | //! let fs_mod = await import('greco://fs');\ 21 | //! await fs_mod.write('./test.txt', 'hello from greco fs'); 22 | //! }\n")).expect("script failed"); 23 | //! let prom_jsvf = rt.invoke_function_sync(None, &[], "test_write", vec![]).ok().expect("write function invocation failed"); 24 | //! // wait for promise to be done 25 | //! 26 | //! if let JsValueFacade::JsPromise { cached_promise } = prom_jsvf { 27 | //! let done = block_on(cached_promise.get_promise_result()); 28 | //! assert!(done.is_ok()); 29 | //! } else { 30 | //! panic!("not a promise"); 31 | //! } 32 | //! 33 | //! // do read test 34 | //! let eval_fut = rt.eval(None, Script::new("init_fs.es", "async function test_read() {\ 35 | //! let fs_mod = await import('greco://fs');\ 36 | //! return await fs_mod.readString('./test.txt'); 37 | //! }\n")); 38 | //! let _ = block_on(eval_fut); 39 | //! let prom_jsvf = rt.invoke_function_sync(None, &[], "test_read", vec![]).ok().expect("read invocation failed"); 40 | //! // wait for promise to be done 41 | //! if let JsValueFacade::JsPromise { cached_promise } = prom_jsvf { 42 | //! let done = block_on(cached_promise.get_promise_result()).ok().expect("prom failed"); 43 | //! match done { 44 | //! Ok(done_jsvf) => { 45 | //! let s = done_jsvf.stringify(); 46 | //! assert_eq!(s, "String: hello from greco fs"); 47 | //! } 48 | //! Err(val) => { 49 | //! panic!("promise was rejected: {}", val.stringify()); 50 | //! } 51 | //! } 52 | //! 53 | //! } else { 54 | //! panic!("not promise") 55 | //! } 56 | //! ``` 57 | //! 58 | //! # Methods 59 | //! 60 | //! ##append 61 | //! ##copy 62 | //! ##createSymlink 63 | //! ##createDirs 64 | //! ##getMetadata 65 | //! ##getSymlinkMetadata 66 | //! ##list 67 | //! ##readString 68 | //! ##removeDir 69 | //! ##removeFile 70 | //! ##rename 71 | //! ##touch 72 | //! ##write 73 | //! 74 | 75 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 76 | use quickjs_runtime::jsutils::modules::NativeModuleLoader; 77 | use quickjs_runtime::jsutils::JsError; 78 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 79 | use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 80 | use quickjs_runtime::values::JsValueFacade; 81 | use std::fs; 82 | 83 | pub(crate) fn read_string(args: &[JsValueFacade]) -> Result { 84 | if args.len() != 1 || !args[0].is_string() { 85 | Err(JsError::new_str( 86 | "readString requires one argument: (String)", 87 | )) 88 | } else { 89 | let path = args[0].get_str(); 90 | 91 | match fs::read_to_string(path) { 92 | Ok(s) => Ok(JsValueFacade::new_string(s)), 93 | Err(e) => Err(JsError::new_string(format!("{e:?}"))), 94 | } 95 | } 96 | } 97 | 98 | pub(crate) fn remove_file(args: &[JsValueFacade]) -> Result { 99 | if args.len() != 1 || !args[0].is_string() { 100 | Err(JsError::new_str( 101 | "removeFile requires one argument: (String)", 102 | )) 103 | } else { 104 | let path = args[0].get_str(); 105 | 106 | match fs::remove_file(path) { 107 | Ok(_) => Ok(JsValueFacade::Null), 108 | Err(e) => Err(JsError::new_string(format!("{e:?}"))), 109 | } 110 | } 111 | } 112 | 113 | pub(crate) fn append(_args: &[JsValueFacade]) -> Result { 114 | unimplemented!() 115 | } 116 | 117 | pub(crate) fn copy(_args: &[JsValueFacade]) -> Result { 118 | unimplemented!() 119 | } 120 | 121 | pub(crate) fn create_symlink(_args: &[JsValueFacade]) -> Result { 122 | unimplemented!() 123 | } 124 | 125 | pub(crate) fn create_dirs(_args: &[JsValueFacade]) -> Result { 126 | unimplemented!() 127 | } 128 | 129 | pub(crate) fn get_metadata(_args: &[JsValueFacade]) -> Result { 130 | unimplemented!() 131 | } 132 | 133 | pub(crate) fn get_symlink_metadata(_args: &[JsValueFacade]) -> Result { 134 | unimplemented!() 135 | } 136 | 137 | pub(crate) fn list(_args: &[JsValueFacade]) -> Result { 138 | unimplemented!() 139 | } 140 | 141 | pub(crate) fn remove_dir(_args: &[JsValueFacade]) -> Result { 142 | unimplemented!() 143 | } 144 | 145 | pub(crate) fn rename(_args: &[JsValueFacade]) -> Result { 146 | unimplemented!() 147 | } 148 | 149 | pub(crate) fn touch(_args: &[JsValueFacade]) -> Result { 150 | unimplemented!() 151 | } 152 | 153 | /// write 154 | /// write to a file 155 | /// # Example 156 | /// ```javascript 157 | /// async function write_example() { 158 | /// let fs = await import('greco://fs'); 159 | /// await fs.write('./test.txt', 'hello world'); 160 | /// } 161 | /// ``` 162 | pub(crate) fn write(args: &[JsValueFacade]) -> Result { 163 | if args.len() != 2 || !args[0].is_string() { 164 | Err(JsError::new_str( 165 | "write requires two arguments: (String, obj)", 166 | )) 167 | } else { 168 | let path = args[0].get_str(); 169 | let content = if args[1].is_string() { 170 | args[1].get_str().to_string() 171 | } else { 172 | args[1].stringify() 173 | }; 174 | 175 | match fs::write(path, content) { 176 | Ok(_) => Ok(JsValueFacade::Null), 177 | Err(e) => Err(JsError::new_string(format!("{e:?}"))), 178 | } 179 | } 180 | } 181 | 182 | pub struct FsModuleLoader {} 183 | 184 | impl NativeModuleLoader for FsModuleLoader { 185 | fn has_module(&self, _realm: &QuickJsRealmAdapter, module_name: &str) -> bool { 186 | module_name.eq("greco://fs") 187 | } 188 | 189 | fn get_module_export_names( 190 | &self, 191 | _realm: &QuickJsRealmAdapter, 192 | _module_name: &str, 193 | ) -> Vec<&str> { 194 | vec![ 195 | "append", 196 | "copy", 197 | "createSymlink", 198 | "createDirs", 199 | "getMetadata", 200 | "getSymlinkMetadata", 201 | "list", 202 | "readString", 203 | "removeDir", 204 | "removeFile", 205 | "rename", 206 | "touch", 207 | "write", 208 | ] 209 | } 210 | 211 | fn get_module_exports( 212 | &self, 213 | realm: &QuickJsRealmAdapter, 214 | _module_name: &str, 215 | ) -> Vec<(&str, QuickJsValueAdapter)> { 216 | init_exports(realm).expect("init fs exports failed") 217 | } 218 | } 219 | 220 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 221 | builder.native_module_loader(FsModuleLoader {}) 222 | } 223 | 224 | fn init_exports( 225 | realm: &QuickJsRealmAdapter, 226 | ) -> Result, JsError> { 227 | let copy_func = JsValueFacade::new_function("copy", copy, 1); 228 | let write_func = JsValueFacade::new_function("write", write, 1); 229 | let append_func = JsValueFacade::new_function("append", append, 1); 230 | let create_symlink_func = JsValueFacade::new_function("createSymlink", create_symlink, 1); 231 | let create_dirs_func = JsValueFacade::new_function("createDirs", create_dirs, 1); 232 | let get_metadata_func = JsValueFacade::new_function("getMetadata", get_metadata, 1); 233 | let get_symlink_metadata_func = 234 | JsValueFacade::new_function("getSymlinkMetadata", get_symlink_metadata, 1); 235 | let list_func = JsValueFacade::new_function("list", list, 1); 236 | let remove_dir_func = JsValueFacade::new_function("removeDir", remove_dir, 1); 237 | let rename_func = JsValueFacade::new_function("rename", rename, 1); 238 | let touch_func = JsValueFacade::new_function("touch", touch, 1); 239 | let remove_file_func = JsValueFacade::new_function("removeFile", remove_file, 1); 240 | let read_string_func = JsValueFacade::new_function("readString", read_string, 1); 241 | 242 | Ok(vec![ 243 | ("write", realm.from_js_value_facade(write_func)?), 244 | ( 245 | "getSymlinkMetadata", 246 | realm.from_js_value_facade(get_symlink_metadata_func)?, 247 | ), 248 | ("copy", realm.from_js_value_facade(copy_func)?), 249 | ("append", realm.from_js_value_facade(append_func)?), 250 | ( 251 | "createSymlink", 252 | realm.from_js_value_facade(create_symlink_func)?, 253 | ), 254 | ("createDirs", realm.from_js_value_facade(create_dirs_func)?), 255 | ( 256 | "getMetadata", 257 | realm.from_js_value_facade(get_metadata_func)?, 258 | ), 259 | ("list", realm.from_js_value_facade(list_func)?), 260 | ("removeDir", realm.from_js_value_facade(remove_dir_func)?), 261 | ("rename", realm.from_js_value_facade(rename_func)?), 262 | ("touch", realm.from_js_value_facade(touch_func)?), 263 | ("removeFile", realm.from_js_value_facade(remove_file_func)?), 264 | ("readString", realm.from_js_value_facade(read_string_func)?), 265 | ]) 266 | } 267 | 268 | #[cfg(test)] 269 | pub mod tests { 270 | use crate::init_greco_rt; 271 | use backtrace::Backtrace; 272 | use log::LevelFilter; 273 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 274 | use quickjs_runtime::jsutils::Script; 275 | use quickjs_runtime::values::JsValueFacade; 276 | use std::panic; 277 | 278 | #[test] 279 | fn test_fs() { 280 | panic::set_hook(Box::new(|panic_info| { 281 | let backtrace = Backtrace::new(); 282 | log::error!( 283 | "thread panic occurred: {}\nbacktrace: {:?}", 284 | panic_info, 285 | backtrace 286 | ); 287 | })); 288 | 289 | simple_logging::log_to_file("grecort.log", LevelFilter::max()) 290 | .ok() 291 | .expect("could not init logger"); 292 | 293 | let mut rtb = QuickJsRuntimeBuilder::new(); 294 | rtb = init_greco_rt(rtb); 295 | let rt = rtb.build(); 296 | rt.eval_sync( 297 | None, 298 | Script::new( 299 | "init_fs.es", 300 | "async function test_write() {\ 301 | let fs_mod = await import('greco://fs');\ 302 | await fs_mod.write('./test.txt', 'hello from greco fs'); 303 | }\n", 304 | ), 305 | ) 306 | .ok() 307 | .expect("init write script failed"); 308 | let prom_esvf = rt 309 | .invoke_function_sync(None, &[], "test_write", vec![]) 310 | .ok() 311 | .expect("write function invocation failed"); 312 | // wait for promise to be done 313 | 314 | assert!(prom_esvf.is_js_promise()); 315 | 316 | if let JsValueFacade::JsPromise { cached_promise } = prom_esvf { 317 | let done = cached_promise 318 | .get_promise_result_sync() 319 | .expect("promise timed out"); 320 | assert!(done.is_ok()); 321 | } 322 | 323 | // do read test 324 | rt.eval_sync( 325 | None, 326 | Script::new( 327 | "init_fs.es", 328 | "async function test_read() {\ 329 | let fs_mod = await import('greco://fs');\ 330 | return await fs_mod.readString('./test.txt'); 331 | }\n", 332 | ), 333 | ) 334 | .ok() 335 | .expect("init write script failed"); 336 | let prom_esvf = rt 337 | .invoke_function_sync(None, &[], "test_read", vec![]) 338 | .ok() 339 | .expect("read invocation failed"); 340 | // wait for promise to be done 341 | 342 | assert!(prom_esvf.is_js_promise()); 343 | if let JsValueFacade::JsPromise { cached_promise } = prom_esvf { 344 | let done = cached_promise 345 | .get_promise_result_sync() 346 | .expect("promise timed out"); 347 | assert!(done.is_ok()); 348 | let done_esvf = done.ok().unwrap(); 349 | let s = done_esvf.get_str(); 350 | assert_eq!(s, "hello from greco fs"); 351 | } 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/moduleloaders/mod.rs: -------------------------------------------------------------------------------- 1 | use log::trace; 2 | use quickjs_runtime::jsutils::modules::ScriptModuleLoader; 3 | use quickjs_runtime::jsutils::JsError; 4 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 5 | use std::fs; 6 | use std::ops::Add; 7 | use std::path::{Path, PathBuf}; 8 | use url::Url; 9 | 10 | pub struct FileSystemModuleLoader { 11 | base_path: PathBuf, 12 | } 13 | 14 | fn last_index_of(haystack: &str, needle: &str) -> Option { 15 | let start = haystack.len() - needle.len(); 16 | let mut x = start; 17 | loop { 18 | if haystack[x..(x + needle.len())].eq(needle) { 19 | return Some(x); 20 | } 21 | if x == 0 { 22 | break; 23 | } 24 | x -= 1; 25 | } 26 | None 27 | } 28 | 29 | pub fn normalize_path(ref_path: &str, name: &str) -> Result { 30 | // todo support: 31 | // name starting with / 32 | // name starting or containing ../ or starting with ./ 33 | 34 | let ref_path = if let Some(last_slash_idx) = last_index_of(ref_path, "/") { 35 | let mut path = ref_path.to_string(); 36 | let _file_name = path.split_off(last_slash_idx); 37 | path 38 | } else { 39 | ref_path.to_string() 40 | }; 41 | 42 | let url = Url::parse(ref_path.as_str()).map_err(|e| { 43 | JsError::new_string(format!("failed to parse Url [{ref_path}] due to : {e}")) 44 | })?; 45 | let path = if let Some(stripped) = name.strip_prefix('/') { 46 | stripped.to_string() 47 | } else { 48 | let url_path = url.path(); 49 | if url_path.eq("/") { 50 | name.to_string() 51 | } else { 52 | format!("{}/{}", &url_path[1..], name) 53 | } 54 | }; 55 | 56 | // remove ./ 57 | // remove .. 58 | let mut path_parts: Vec = path.split('/').map(|s| s.to_string()).collect(); 59 | 60 | let mut x = 1; 61 | while x < path_parts.len() { 62 | if path_parts[x].as_str().eq("..") { 63 | path_parts.remove(x); 64 | path_parts.remove(x - 1); 65 | x = 0; 66 | } 67 | if path_parts[x].as_str().eq(".") { 68 | path_parts.remove(x); 69 | x = 0; 70 | } 71 | x += 1; 72 | } 73 | let path = path_parts.join("/"); 74 | 75 | let mut res = url.scheme().to_string(); 76 | res = res.add("://"); 77 | if let Some(host) = url.host_str() { 78 | res = res.add(host); 79 | if let Some(port) = url.port() { 80 | res = res.add(format!(":{port}").as_str()); 81 | } 82 | } 83 | res = res.add("/"); 84 | 85 | res = res.add(path.as_str()); 86 | 87 | log::debug!("normalize_path returning: {}", res); 88 | 89 | Ok(res) 90 | } 91 | 92 | impl FileSystemModuleLoader { 93 | pub fn new(base_path: &'static str) -> Self { 94 | log::trace!("FileSystemModuleLoader::new {}", base_path); 95 | Self { 96 | base_path: Path::new(base_path).canonicalize().expect("path not found"), 97 | } 98 | } 99 | 100 | fn get_real_fs_path(&self, abs_file_path: &str) -> PathBuf { 101 | assert!(abs_file_path.starts_with("file:///")); 102 | self.base_path.join(Path::new(&abs_file_path[8..])) 103 | } 104 | 105 | fn read_file(&self, filename: &str) -> Result { 106 | trace!("FileSystemModuleLoader::read_file -> {}", filename); 107 | 108 | let path = self.get_real_fs_path(filename); 109 | if !path.exists() { 110 | return Err(format!("File not found: {filename}")); 111 | } 112 | let path = path.canonicalize().unwrap(); 113 | if !path.starts_with(&self.base_path) { 114 | return Err(format!("File not allowed: {filename}")); 115 | } 116 | 117 | fs::read_to_string(path).map_err(|e| format!("failed to read: {filename}, caused by: {e}")) 118 | } 119 | 120 | fn file_exists(&self, filename: &str) -> bool { 121 | trace!("FileSystemModuleLoader::file_exists -> {}", filename); 122 | let path = self.get_real_fs_path(filename); 123 | path.exists() && path.canonicalize().unwrap().starts_with(&self.base_path) 124 | } 125 | fn normalize_file_path(&self, ref_path: &str, path: &str) -> Option { 126 | // the ref path will always be an absolute path, so no need to parse . or .. 127 | // but even though we call it an absolute path here it will will be a relative path to the loader's main dir 128 | // so basically the file:// prefix is just to recognize the path a a path the FileSystemModuleLoader can handle 129 | 130 | if !ref_path.starts_with("file://") { 131 | return None; 132 | } 133 | if path.starts_with("file://") { 134 | return Some(path.to_string()); 135 | } 136 | if path.contains("://") && !path.starts_with("file://") { 137 | // e.g. including a http:// based module from a file based module, should be handled by http loader 138 | return None; 139 | } 140 | 141 | match normalize_path(ref_path, path) { 142 | Ok(normalized) => { 143 | if self.file_exists(normalized.as_str()) { 144 | Some(normalized) 145 | } else { 146 | // todo support other module extensions 147 | let ts_opt = format!("{normalized}.ts"); 148 | if self.file_exists(ts_opt.as_str()) { 149 | Some(ts_opt) 150 | } else { 151 | None 152 | } 153 | } 154 | } 155 | Err(e) => { 156 | log::error!("could not normalize {}: {}", path, e); 157 | None 158 | } 159 | } 160 | } 161 | } 162 | 163 | impl ScriptModuleLoader for FileSystemModuleLoader { 164 | fn normalize_path( 165 | &self, 166 | _realm: &QuickJsRealmAdapter, 167 | ref_path: &str, 168 | path: &str, 169 | ) -> Option { 170 | self.normalize_file_path(ref_path, path) 171 | } 172 | 173 | fn load_module(&self, _realm: &QuickJsRealmAdapter, absolute_path: &str) -> String { 174 | self.read_file(absolute_path) 175 | .unwrap_or_else(|_| "".to_string()) 176 | } 177 | } 178 | 179 | #[cfg(any(feature = "all", feature = "com", feature = "http"))] 180 | pub struct HttpModuleLoader { 181 | is_secure_only: bool, 182 | is_validate_content_type: bool, 183 | allowed_domains: Option>, 184 | _basic_auth: Option<(String, String)>, 185 | // todo stuff like clientcert / servercert checking 186 | } 187 | 188 | #[cfg(any(feature = "all", feature = "com", feature = "http"))] 189 | impl HttpModuleLoader { 190 | pub fn new() -> Self { 191 | Self { 192 | is_secure_only: false, 193 | is_validate_content_type: true, 194 | allowed_domains: None, 195 | _basic_auth: None, 196 | } 197 | } 198 | 199 | pub fn secure_only(mut self) -> Self { 200 | self.is_secure_only = true; 201 | self 202 | } 203 | 204 | pub fn validate_content_type(mut self, validate: bool) -> Self { 205 | self.is_validate_content_type = validate; 206 | self 207 | } 208 | 209 | pub fn allow_domain(mut self, domain: &str) -> Self { 210 | if self.allowed_domains.is_none() { 211 | self.allowed_domains = Some(vec![]); 212 | } 213 | let domains = self.allowed_domains.as_mut().unwrap(); 214 | domains.push(domain.to_string()); 215 | self 216 | } 217 | 218 | fn read_url(&self, url: &str) -> Option { 219 | let resp = reqwest::blocking::get(url); 220 | //let req = reqwest::get(url); 221 | // todo make read_url async 222 | if resp.is_err() { 223 | return None; 224 | } 225 | let resp = resp.expect("wtf"); 226 | if self.is_validate_content_type { 227 | let ct = &resp.headers()["Content-Type"]; 228 | if !(ct.eq("application/javascript") || ct.eq("text/javascript")) { 229 | log::error!("loaded module {} did not have javascript Content-Type", url); 230 | return None; 231 | } 232 | } 233 | // todo async 234 | let res = resp.text(); 235 | match res { 236 | Ok(script) => Some(script), 237 | Err(e) => { 238 | log::error!("could not load {} due to: {}", url, e); 239 | None 240 | } 241 | } 242 | } 243 | 244 | fn is_allowed(&self, absolute_path: &str) -> bool { 245 | if self.is_secure_only || self.allowed_domains.is_some() { 246 | match Url::parse(absolute_path) { 247 | Ok(url) => { 248 | if self.is_secure_only && !url.scheme().eq("https") { 249 | false 250 | } else if let Some(domains) = &self.allowed_domains { 251 | if let Some(host) = url.host_str() { 252 | domains.contains(&host.to_string()) 253 | } else { 254 | false 255 | } 256 | } else { 257 | true 258 | } 259 | } 260 | Err(e) => { 261 | log::error!( 262 | "HttpModuleLoader.is_allowed: could not parse url: {}, {}", 263 | absolute_path, 264 | e 265 | ); 266 | false 267 | } 268 | } 269 | } else { 270 | true 271 | } 272 | } 273 | 274 | fn normalize_http_path(&self, ref_path: &str, path: &str) -> Option { 275 | // the ref path will always be an absolute path 276 | 277 | if path.starts_with("http://") || path.starts_with("https://") { 278 | return if self.is_allowed(path) { 279 | Some(path.to_string()) 280 | } else { 281 | None 282 | }; 283 | } 284 | 285 | if path.contains("://") { 286 | return None; 287 | } 288 | 289 | if !(ref_path.starts_with("http://") || ref_path.starts_with("https://")) { 290 | return None; 291 | } 292 | 293 | match normalize_path(ref_path, path) { 294 | Ok(normalized) => { 295 | if self.is_allowed(normalized.as_str()) { 296 | Some(normalized) 297 | } else { 298 | None 299 | } 300 | } 301 | Err(e) => { 302 | log::error!("could not normalize: {}: {}", path, e); 303 | None 304 | } 305 | } 306 | } 307 | } 308 | 309 | #[cfg(any(feature = "all", feature = "com", feature = "http"))] 310 | impl Default for HttpModuleLoader { 311 | fn default() -> Self { 312 | Self::new() 313 | } 314 | } 315 | 316 | #[cfg(any(feature = "all", feature = "com", feature = "http"))] 317 | impl ScriptModuleLoader for HttpModuleLoader { 318 | fn normalize_path( 319 | &self, 320 | _realm: &QuickJsRealmAdapter, 321 | ref_path: &str, 322 | path: &str, 323 | ) -> Option { 324 | self.normalize_http_path(ref_path, path) 325 | } 326 | 327 | fn load_module(&self, _realm: &QuickJsRealmAdapter, absolute_path: &str) -> String { 328 | // todo, load_module should really return a Result 329 | if let Some(script) = self.read_url(absolute_path) { 330 | script 331 | } else { 332 | "".to_string() 333 | } 334 | } 335 | } 336 | 337 | #[cfg(test)] 338 | mod tests { 339 | use crate::moduleloaders::{ 340 | last_index_of, normalize_path, FileSystemModuleLoader, HttpModuleLoader, 341 | }; 342 | use std::path::Path; 343 | 344 | #[test] 345 | fn test_last_index_of() { 346 | assert_eq!(last_index_of("abcba", "b").unwrap(), 3); 347 | assert_eq!(last_index_of("abbcbba", "bb").unwrap(), 4); 348 | } 349 | 350 | #[test] 351 | fn test_normalize() { 352 | { 353 | assert_eq!( 354 | normalize_path("http://test.com/scripts/foo.es", "bar.mes") 355 | .ok() 356 | .unwrap(), 357 | "http://test.com/scripts/bar.mes" 358 | ); 359 | assert_eq!( 360 | normalize_path("http://test.com/scripts/foo.es", "/bar.mes") 361 | .ok() 362 | .unwrap(), 363 | "http://test.com/bar.mes" 364 | ); 365 | assert_eq!( 366 | normalize_path("http://test.com/scripts/foo.es", "../bar.mes") 367 | .ok() 368 | .unwrap(), 369 | "http://test.com/bar.mes" 370 | ); 371 | assert_eq!( 372 | normalize_path("http://test.com/scripts/foo.es", "./bar.mes") 373 | .ok() 374 | .unwrap(), 375 | "http://test.com/scripts/bar.mes" 376 | ); 377 | assert_eq!( 378 | normalize_path("file:///scripts/test.es", "bar.mes") 379 | .ok() 380 | .unwrap(), 381 | "file:///scripts/bar.mes" 382 | ); 383 | assert_eq!( 384 | normalize_path("file:///scripts/test.es", "./bar.mes") 385 | .ok() 386 | .unwrap(), 387 | "file:///scripts/bar.mes" 388 | ); 389 | assert_eq!( 390 | normalize_path("file:///scripts/test.es", "../bar.mes") 391 | .ok() 392 | .unwrap(), 393 | "file:///bar.mes" 394 | ); 395 | } 396 | } 397 | 398 | #[test] 399 | fn test_http() { 400 | let loader = HttpModuleLoader::new() 401 | .secure_only() 402 | .validate_content_type(false) 403 | .allow_domain("github.com") 404 | .allow_domain("httpbin.org"); 405 | // disallow http 406 | assert!(loader 407 | .normalize_http_path("http://github.com/example.js", "module.mjs") 408 | .is_none()); 409 | // disallow domain 410 | assert!(loader 411 | .normalize_http_path("https://other.github.com/example.js", "module.mjs") 412 | .is_none()); 413 | // allow domain 414 | assert!(loader 415 | .normalize_http_path("https://github.com/example.js", "module.mjs") 416 | .is_some()); 417 | assert_eq!( 418 | loader 419 | .normalize_http_path("https://github.com/scripts/example.js", "module.mjs") 420 | .unwrap(), 421 | "https://github.com/scripts/module.mjs" 422 | ); 423 | assert_eq!( 424 | loader 425 | .normalize_http_path("https://github.com/example.js", "module.mjs") 426 | .unwrap(), 427 | "https://github.com/module.mjs" 428 | ); 429 | } 430 | 431 | #[test] 432 | fn test_fs() { 433 | let loader = FileSystemModuleLoader::new("./modules"); 434 | let path = Path::new("./modules").canonicalize().unwrap(); 435 | println!("path = {path:?}"); 436 | assert!(loader 437 | .normalize_file_path("file:///test.es", "utils/assertions.mes") 438 | .is_some()); 439 | assert!(loader 440 | .normalize_file_path("file:///test.es", "utils/notfound.mes") 441 | .is_none()); 442 | } 443 | 444 | #[test] 445 | fn test_gcs() { 446 | match normalize_path("gcsproject:///hello/world.ts", "../project2/world") { 447 | Ok(p) => { 448 | assert_eq!(p.as_str(), "gcsproject:///project2/world") 449 | } 450 | Err(e) => { 451 | panic!("{}", e) 452 | } 453 | } 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/features/js_fetch/spec.rs: -------------------------------------------------------------------------------- 1 | //! fetch api support 2 | //! 3 | //! 4 | //! 5 | 6 | use crate::features::js_fetch::proxies::RESPONSE_INSTANCES; 7 | use quickjs_runtime::jsutils::JsError; 8 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 9 | use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 10 | use std::collections::HashMap; 11 | use std::str::FromStr; 12 | use std::sync::Arc; 13 | 14 | // todo see stackoverflow.com/questions/44121783 15 | pub enum Mode { 16 | Cors, 17 | NoCors, 18 | SameOrigin, 19 | } 20 | 21 | impl Mode { 22 | pub fn as_str(&self) -> &'static str { 23 | match self { 24 | Self::Cors => "cors", 25 | Self::NoCors => "no-cors", 26 | Self::SameOrigin => "same-origin", 27 | } 28 | } 29 | } 30 | 31 | impl FromStr for Mode { 32 | type Err = (); 33 | 34 | fn from_str(val: &str) -> Result { 35 | match val { 36 | "cors" => Ok(Self::Cors), 37 | "no-cors" => Ok(Self::NoCors), 38 | "same-origin" => Ok(Self::SameOrigin), 39 | _ => Err(()), 40 | } 41 | } 42 | } 43 | 44 | pub enum Method { 45 | Get, 46 | Head, 47 | Post, 48 | Put, 49 | Delete, 50 | Connect, 51 | Options, 52 | Trace, 53 | Patch, 54 | Copy, 55 | Lock, 56 | Mkcol, 57 | Move, 58 | Propfind, 59 | Proppatch, 60 | Unlock, 61 | } 62 | 63 | impl Method { 64 | pub fn as_str(&self) -> &'static str { 65 | match self { 66 | Method::Get => "GET", 67 | Method::Head => "HEAD", 68 | Method::Post => "POST", 69 | Method::Put => "PUT", 70 | Method::Delete => "DELETE", 71 | Method::Connect => "CONNECT", 72 | Method::Options => "OPTIONS", 73 | Method::Trace => "TRACE", 74 | Method::Patch => "PATCH", 75 | Method::Copy => "COPY", 76 | Method::Lock => "LOCK", 77 | Method::Mkcol => "MKCOL", 78 | Method::Move => "MOVE", 79 | Method::Propfind => "PROPFIND", 80 | Method::Proppatch => "PROPPATCH", 81 | Method::Unlock => "UNLOCK", 82 | } 83 | } 84 | } 85 | 86 | impl FromStr for Method { 87 | type Err = (); 88 | 89 | fn from_str(val: &str) -> Result { 90 | match val.to_ascii_uppercase().as_str() { 91 | "GET" => Ok(Self::Get), 92 | "HEAD" => Ok(Self::Head), 93 | "POST" => Ok(Self::Post), 94 | "PUT" => Ok(Self::Put), 95 | "DELETE" => Ok(Self::Delete), 96 | "CONNECT" => Ok(Self::Connect), 97 | "OPTIONS" => Ok(Self::Options), 98 | "TRACE" => Ok(Self::Trace), 99 | "PATCH" => Ok(Self::Patch), 100 | "COPY" => Ok(Self::Copy), 101 | "LOCK" => Ok(Self::Lock), 102 | "MKCOL" => Ok(Self::Mkcol), 103 | "MOVE" => Ok(Self::Move), 104 | "PROPFIND" => Ok(Self::Propfind), 105 | "PROPPATCH" => Ok(Self::Proppatch), 106 | "UNLOCK" => Ok(Self::Unlock), 107 | 108 | _ => Err(()), 109 | } 110 | } 111 | } 112 | 113 | pub enum Redirect { 114 | Follow, 115 | Manual, 116 | Error, 117 | } 118 | 119 | impl Redirect { 120 | pub fn as_str(&self) -> &'static str { 121 | match self { 122 | Self::Follow => "follow", 123 | Self::Manual => "manual", 124 | Self::Error => "error", 125 | } 126 | } 127 | } 128 | 129 | impl FromStr for Redirect { 130 | type Err = (); 131 | 132 | fn from_str(val: &str) -> Result { 133 | match val { 134 | "manual" => Ok(Self::Manual), 135 | "follow" => Ok(Self::Follow), 136 | "error" => Ok(Self::Error), 137 | _ => Err(()), 138 | } 139 | } 140 | } 141 | 142 | pub enum Credentials { 143 | Omit, 144 | SameOrigin, 145 | Include, 146 | } 147 | 148 | impl Credentials { 149 | pub fn as_str(&self) -> &'static str { 150 | match self { 151 | Self::Omit => "omit", 152 | Self::SameOrigin => "same-origin", 153 | Self::Include => "include", 154 | } 155 | } 156 | } 157 | 158 | impl FromStr for Credentials { 159 | type Err = (); 160 | 161 | fn from_str(val: &str) -> Result { 162 | match val { 163 | "omit" => Ok(Self::Omit), 164 | "same-origin" => Ok(Self::SameOrigin), 165 | "include" => Ok(Self::Include), 166 | _ => Err(()), 167 | } 168 | } 169 | } 170 | 171 | pub enum Cache { 172 | Default, 173 | NoStore, 174 | Reload, 175 | NoCache, 176 | ForceCache, 177 | OnlyIfCached, 178 | } 179 | 180 | impl Cache { 181 | pub fn as_str(&self) -> &'static str { 182 | match self { 183 | Cache::Default => "default", 184 | Cache::NoStore => "no-store", 185 | Cache::Reload => "reload", 186 | Cache::NoCache => "no-cache", 187 | Cache::ForceCache => "force-cache", 188 | Cache::OnlyIfCached => "only-if-cached", 189 | } 190 | } 191 | } 192 | 193 | impl FromStr for Cache { 194 | type Err = (); 195 | 196 | fn from_str(s: &str) -> Result { 197 | match s { 198 | "default" => Ok(Self::Default), 199 | "no-store" => Ok(Self::NoStore), 200 | "reload" => Ok(Self::Reload), 201 | "no-cache" => Ok(Self::NoCache), 202 | "force-cache" => Ok(Self::ForceCache), 203 | "only-if-cached" => Ok(Self::OnlyIfCached), 204 | _ => Err(()), 205 | } 206 | } 207 | } 208 | 209 | pub struct FetchInit { 210 | method: Method, 211 | headers: Headers, 212 | body: Option, 213 | mode: Mode, 214 | credentials: Credentials, 215 | cache: Cache, 216 | redirect: Redirect, 217 | } 218 | impl FetchInit { 219 | pub fn from_js_object( 220 | realm: &QuickJsRealmAdapter, 221 | value: Option<&QuickJsValueAdapter>, 222 | ) -> Result { 223 | let mut fetch_init = Self { 224 | method: Method::Get, 225 | headers: Headers::new(), 226 | body: None, 227 | mode: Mode::NoCors, 228 | credentials: Credentials::SameOrigin, 229 | cache: Cache::Default, 230 | redirect: Redirect::Follow, 231 | }; 232 | 233 | if let Some(init_obj) = value { 234 | realm.traverse_object_mut(init_obj, |prop_name, prop| { 235 | // 236 | 237 | match prop_name { 238 | "method" => { 239 | let val = prop.to_string()?; 240 | fetch_init.method = Method::from_str(val.as_str()) 241 | .map_err(|_e| JsError::new_str("No such method"))?; 242 | } 243 | "mode" => { 244 | let val = prop.to_string()?; 245 | fetch_init.mode = Mode::from_str(val.as_str()) 246 | .map_err(|_e| JsError::new_str("No such mode"))?; 247 | } 248 | "cache" => { 249 | let val = prop.to_string()?; 250 | fetch_init.cache = Cache::from_str(val.as_str()) 251 | .map_err(|_e| JsError::new_str("No such cache"))?; 252 | } 253 | "credentials" => { 254 | let val = prop.to_string()?; 255 | fetch_init.credentials = Credentials::from_str(val.as_str()) 256 | .map_err(|_e| JsError::new_str("No such credentials"))?; 257 | } 258 | 259 | "redirect" => { 260 | let val = prop.to_string()?; 261 | fetch_init.redirect = Redirect::from_str(val.as_str()) 262 | .map_err(|_e| JsError::new_str("No such redirect"))?; 263 | } 264 | 265 | "body" => { 266 | if prop.is_string() { 267 | let val = prop.to_string()?; 268 | fetch_init.body = Some(Body { 269 | text: Some(val), 270 | bytes: None, 271 | }); 272 | } 273 | if prop.is_typed_array() { 274 | let val = realm.copy_typed_array_buffer(prop)?; 275 | fetch_init.body = Some(Body { 276 | bytes: Some(val), 277 | text: None, 278 | }); 279 | } 280 | } 281 | "headers" => { 282 | realm.traverse_object_mut(prop, |header_name, header_val| { 283 | fetch_init 284 | .headers 285 | .append(header_name, header_val.to_string()?.as_str()); 286 | Ok(()) 287 | })?; 288 | } 289 | 290 | _ => {} 291 | } 292 | 293 | Ok(()) 294 | })?; 295 | } 296 | Ok(fetch_init) 297 | } 298 | } 299 | 300 | pub struct Headers { 301 | map: HashMap>, 302 | } 303 | impl Headers { 304 | pub fn new() -> Self { 305 | Self { 306 | map: Default::default(), 307 | } 308 | } 309 | pub fn append(&mut self, name: &str, value: &str) { 310 | if !self.map.contains_key(name) { 311 | self.map.insert(name.to_string(), vec![]); 312 | } 313 | let vec = self.map.get_mut(name).unwrap(); 314 | vec.push(value.to_string()); 315 | } 316 | pub fn get(&self, name: &str) -> Option<&Vec> { 317 | self.map.get(name) 318 | } 319 | } 320 | impl Default for Headers { 321 | fn default() -> Self { 322 | Self::new() 323 | } 324 | } 325 | 326 | pub struct Body { 327 | pub text: Option, 328 | pub bytes: Option>, 329 | } 330 | impl Body { 331 | // 332 | } 333 | 334 | pub struct Response { 335 | pub body: Body, 336 | pub headers: Headers, 337 | pub ok: bool, 338 | pub redirected: bool, 339 | pub status: u16, 340 | pub status_text: &'static str, 341 | pub response_type: &'static str, 342 | pub url: String, 343 | } 344 | impl Response { 345 | pub fn to_js_value(self, realm: &QuickJsRealmAdapter) -> Result { 346 | // todo 347 | let inst_res = realm.instantiate_proxy(&[], "Response", &[])?; 348 | RESPONSE_INSTANCES.with(|rc| { 349 | let map = &mut *rc.borrow_mut(); 350 | map.insert(inst_res.0, Arc::new(self)) 351 | }); 352 | Ok(inst_res.1) 353 | } 354 | pub async fn text(&self) -> Result { 355 | if let Some(text) = self.body.text.as_ref() { 356 | Ok(text.clone()) 357 | } else if let Some(bytes) = self.body.bytes.as_ref() { 358 | Ok(String::from_utf8(bytes.clone()) 359 | .map_err(|_e| JsError::new_str("could not convert to string (utf8 error)"))?) 360 | } else { 361 | Err(JsError::new_str("body had no content")) 362 | } 363 | } 364 | // todo impl some sort of take so we don;t copy bytes every time they are used (ReadableStream and such) 365 | pub async fn bytes(&self) -> Result, JsError> { 366 | if let Some(bytes) = self.body.bytes.as_ref() { 367 | Ok(bytes.clone()) 368 | } else { 369 | Err(JsError::new_str("body had no content")) 370 | } 371 | } 372 | pub async fn form_data(&self) -> Result { 373 | todo!() 374 | } 375 | } 376 | 377 | pub trait Request { 378 | fn get_url(&self) -> &str; 379 | fn get_header(&self, name: &str) -> &[String]; 380 | } 381 | 382 | pub async fn do_fetch(url: Option, fetch_init: FetchInit) -> Result { 383 | let client = reqwest::ClientBuilder::new() 384 | .build() 385 | .map_err(|e| JsError::new_string(format!("{e:?}")))?; 386 | do_fetch2(&client, url, fetch_init).await 387 | } 388 | 389 | pub async fn do_fetch2( 390 | client: &reqwest::Client, 391 | url: Option, 392 | fetch_init: FetchInit, 393 | ) -> Result { 394 | if let Some(url) = url { 395 | let method = reqwest::Method::from_str(fetch_init.method.as_str()) 396 | .map_err(|e| JsError::new_string(format!("{e:?}")))?; 397 | 398 | let mut request = client.request(method, &url); 399 | 400 | if let Some(body) = fetch_init.body { 401 | if let Some(text) = body.text.as_ref() { 402 | request = request.body(text.clone()); 403 | } else if let Some(bytes) = body.bytes.as_ref() { 404 | request = request.body(bytes.clone()); // todo impl .take 405 | } 406 | } 407 | 408 | for header in &fetch_init.headers.map { 409 | for val in header.1 { 410 | request = request.header(header.0, val); 411 | } 412 | } 413 | 414 | let response_fut = request.send(); 415 | 416 | let reqwest_resp = response_fut 417 | .await 418 | .map_err(|e| JsError::new_string(format!("reqwest error {e:?}")))?; 419 | 420 | let mut headers = Headers::new(); 421 | for hv in reqwest_resp.headers() { 422 | headers.map.insert( 423 | hv.0.to_string(), 424 | vec![hv 425 | .1 426 | .to_str() 427 | .map_err(|e| JsError::new_string(format!("{e:?}")))? 428 | .to_string()], 429 | ); 430 | } 431 | 432 | let ok = reqwest_resp.status().is_success(); 433 | let status = reqwest_resp.status().as_u16(); 434 | 435 | let mut is_text = false; 436 | if let Some(ct) = reqwest_resp.headers().get("content-type") { 437 | let ct_str = ct 438 | .to_str() 439 | .map_err(|e| JsError::new_string(format!("{e:?}")))?; 440 | if ct_str.eq("text/plain") 441 | || ct_str.eq("text/html") 442 | || ct_str.eq("application/json") 443 | || ct_str.eq("image/svg+xml") 444 | { 445 | is_text = true; 446 | } 447 | } 448 | 449 | let body = if is_text { 450 | Body { 451 | // todo support bytes, it would make more sense to make reqwest_resp a member of reponse, then we can also impl Response.arrayBuffer() or Response.blob() 452 | text: Some( 453 | reqwest_resp 454 | .text() 455 | .await 456 | .map_err(|e| JsError::new_string(format!("{e:?}")))?, 457 | ), 458 | bytes: None, 459 | } 460 | } else { 461 | let bytes: Vec = reqwest_resp 462 | .bytes() 463 | .await 464 | .map_err(|e| JsError::new_string(format!("{e:?}")))? 465 | .to_vec(); 466 | 467 | Body { 468 | text: None, 469 | bytes: Some(bytes), 470 | } 471 | }; 472 | 473 | let response: Response = Response { 474 | body, 475 | headers, 476 | ok, 477 | redirected: false, 478 | status, 479 | status_text: "", 480 | response_type: "", 481 | url: "".to_string(), 482 | }; 483 | Ok(response) 484 | } else { 485 | Err(JsError::new_str("Missing mandatory url argument")) 486 | } 487 | } 488 | 489 | #[cfg(test)] 490 | pub mod tests { 491 | /* 492 | use futures::executor::block_on; 493 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 494 | use quickjs_runtime::jsutils::Script; 495 | use quickjs_runtime::values::JsValueFacade; 496 | 497 | #[test] 498 | fn test_fetch_1() { 499 | let rt = crate::init_greco_rt(QuickJsRuntimeBuilder::new()).build(); 500 | let mut res = block_on(rt.eval(None, Script::new("test_fetch_1.js", r#" 501 | (async () => { 502 | let res = await fetch("https://httpbin.org/post", {method: "POST", headers:{"Content-Type": "application/json"}, body: JSON.stringify({obj: 1})}); 503 | return res.text(); 504 | })(); 505 | "#))).expect("script failed"); 506 | if let JsValueFacade::JsPromise { cached_promise } = res { 507 | res = block_on(cached_promise.get_promise_result()) 508 | .expect("promise timed out") 509 | .expect("promise failed"); 510 | } 511 | 512 | let str = res.stringify(); 513 | 514 | println!("res: {str}"); 515 | 516 | assert!(str.contains("\"json\": {\n \"obj\": 1\n }")) 517 | }*/ 518 | } 519 | -------------------------------------------------------------------------------- /src/modules/io/gpio/mod.rs: -------------------------------------------------------------------------------- 1 | //! gpio module 2 | //! 3 | //! this module may be loaded in an EsRuntime initialized by green_copper_runtime::new_greco_runtime() by loading 'greco://gpio' 4 | //! 5 | //! # Example 6 | //! 7 | //! * Blink an Led 8 | //! 9 | //! ```javascript 10 | //! async function test_gpio() { 11 | //! // load the module 12 | //! let gpio_mod = await import('greco://gpio'); 13 | //! // create a new PinSet 14 | //! let pin_set = new gpio_mod.PinSet(); 15 | //! // init a single pin 16 | //! await pin_set.init('/dev/gpiochip0', 'out', [13]); 17 | //! // set the state of that pin to 1 (e.g. turn on a led) 18 | //! await pin_set.setState(1); 19 | //! } 20 | //! test_gpio().then(() => { 21 | //! console.log("done testing GPIO"); 22 | //! }).catch((ex) => { 23 | //! console.error("GPIO test failed: %s", "" + ex); 24 | //! }); 25 | //! ``` 26 | //! 27 | //! * Listen for button press 28 | //! 29 | //! ```javascript 30 | //! async function test_gpio() { 31 | //! // load the module 32 | //! let gpio_mod = await import('greco://gpio'); 33 | //! // create a new PinSet 34 | //! let pin_set = new gpio_mod.PinSet(); 35 | //! // init two pins to listen to 36 | //! await pin_set.init('/dev/gpiochip0', 'in', [12, 13]); 37 | //! // add an event listener 38 | //! pin_set.addEventListener('rising', (evt) => { 39 | //! console.log("Pin state went to rising for %s", evt.pin); 40 | //! }); 41 | //! pin_set.addEventListener('falling', (evt) => { 42 | //! console.log("Pin state went to falling for %s", evt.pin);//! 43 | //! }); 44 | //! } 45 | //! test_gpio().then(() => { 46 | //! console.log("done testing GPIO"); 47 | //! }).catch((ex) => { 48 | //! console.error("GPIO test failed: %s", "" + ex); 49 | //! }); 50 | //! ``` 51 | use crate::modules::io::gpio::pinset::{PinMode, PinSet, PinSetHandle}; 52 | use gpio_cdev::EventType; 53 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 54 | use quickjs_runtime::jsutils::jsproxies::JsProxy; 55 | use quickjs_runtime::jsutils::modules::NativeModuleLoader; 56 | use quickjs_runtime::jsutils::{JsError, JsValueType}; 57 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 58 | use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 59 | use quickjs_runtime::values::JsValueFacade; 60 | use std::cell::RefCell; 61 | use std::collections::HashMap; 62 | use std::sync::mpsc::sync_channel; 63 | 64 | pub mod pinset; 65 | 66 | thread_local! { 67 | static PIN_SET_HANDLES: RefCell> = RefCell::new(HashMap::new()); 68 | } 69 | 70 | fn wrap_prom( 71 | realm: &QuickJsRealmAdapter, 72 | instance_id: &usize, 73 | runner: R, 74 | ) -> Result 75 | where 76 | R: FnOnce(&mut PinSet) -> Result + Send + 'static, 77 | { 78 | // reminder, in JS worker thread here, pinset handles are thread_local to that thread. 79 | // handles send info to it's event_loop which is a different dedicated thread 80 | 81 | let fut = PIN_SET_HANDLES.with(|rc| { 82 | let map = &*rc.borrow(); 83 | let handle = map.get(instance_id); 84 | let pin_set_handle = handle.expect("no such pinset"); 85 | pin_set_handle.do_with_mut(move |pin_set| runner(pin_set)) 86 | }); 87 | 88 | realm.create_resolving_promise_async( 89 | async move { 90 | // run async code here and resolve or reject handle 91 | Ok(fut.await) 92 | }, 93 | |realm, res| { 94 | // map 95 | realm.from_js_value_facade(res?) 96 | }, 97 | ) 98 | } 99 | 100 | fn init_exports( 101 | realm: &QuickJsRealmAdapter, 102 | ) -> Result, JsError> { 103 | let pin_set_proxy_class = JsProxy::new().namespace(&["greco", "io", "gpio"]).name("PinSet") 104 | .event_target() 105 | .constructor(|_runtime, _realm, instance_id, _args| { 106 | let pin_set_handle = PinSetHandle::new(); 107 | PIN_SET_HANDLES.with(|rc| { 108 | let handles = &mut *rc.borrow_mut(); 109 | handles.insert(instance_id, pin_set_handle); 110 | }); 111 | Ok(()) 112 | }) 113 | .method("init", |_runtime, realm, instance_id, args| { 114 | // init pins, return prom, reject on fail 115 | let instance_id = *instance_id; 116 | 117 | if args.len() < 3 { 118 | return Err(JsError::new_str("PinSet.init requires 3 args")); 119 | } 120 | if args[0].get_js_type() != JsValueType::String { 121 | return Err(JsError::new_str("PinSet.init first arg should be a String (name of gpio chip e.g. /dev/gpiochip0)")); 122 | } 123 | if args[1].get_js_type() != JsValueType::String { 124 | return Err(JsError::new_str("PinSet.init second arg should be either 'in' or 'out' (for input or output mode)")); 125 | } 126 | if args[2].get_js_type() != JsValueType::Array { 127 | return Err(JsError::new_str("PinSet.init third arg should be an array of pin numbers")); 128 | } 129 | 130 | // todo check arg values... i really need to make an arg assertion util 131 | 132 | let chip_name = args[0].to_string()?; 133 | let mode = args[1].to_string()?; 134 | let pin_mode = if mode.eq("in") {PinMode::In } else {PinMode::Out }; 135 | 136 | let mut pins = vec![]; 137 | 138 | let ct = realm.get_array_length(&args[2])?; 139 | for x in 0..ct { 140 | 141 | let pin_ref = realm.get_array_element(&args[2], x)?; 142 | if pin_ref.get_js_type() != JsValueType::I32 { 143 | return Err(JsError::new_str("pins array should be an array of Numbers")); 144 | } 145 | pins.push(pin_ref.to_i32() as u32); 146 | } 147 | 148 | 149 | 150 | let es_rti_ref = realm.get_runtime_facade_inner(); 151 | let context_id = realm.get_realm_id().to_string(); 152 | 153 | wrap_prom(realm, &instance_id, move |pin_set| { 154 | // in gio eventqueue thread here 155 | pin_set.init(chip_name.as_str(), pin_mode, pins.as_slice()).map_err(|err| {JsError::new_string(err)})?; 156 | 157 | match pin_mode { 158 | PinMode::In => { 159 | log::trace!("init pinset proxy event handler"); 160 | match pin_set.set_event_handler(move |pin, evt| { 161 | log::debug!("called: pinset proxy event handler for pin {} e:{:?}", pin, evt); 162 | let realm_id = context_id.clone(); 163 | 164 | if let Some(es_rt_ref) = es_rti_ref.upgrade() { 165 | es_rt_ref.add_rt_task_to_event_loop_void(move |runtime| { 166 | // in q_js_rt event queue here 167 | if let Some(realm) = runtime.get_realm(realm_id.as_str()) { 168 | // todo evt should be instance of PinSetEvent proxy 169 | let res: Result<(), JsError> = (|| { 170 | let evt_obj = realm.create_object()?; 171 | let pin_ref = realm.create_i32(pin as i32)?; 172 | realm.set_object_property(&evt_obj, "pin", &pin_ref)?; 173 | match evt.event_type() { 174 | EventType::RisingEdge => { 175 | realm.dispatch_proxy_event(&["greco", "io", "gpio"], "PinSet", &instance_id, "rising", &evt_obj)?; 176 | } 177 | EventType::FallingEdge => { 178 | realm.dispatch_proxy_event(&["greco", "io", "gpio"], "PinSet", &instance_id, "falling", &evt_obj)?; 179 | } 180 | } 181 | Ok(()) 182 | })(); 183 | if res.is_err(){ 184 | log::error!("init async action failed: {}", res.err().unwrap()); 185 | } 186 | } else { 187 | log::error!("realm not found"); 188 | } 189 | }); 190 | } 191 | 192 | }) { 193 | Ok(_) => { 194 | log::trace!("init PinSet proxy event handler > ok"); 195 | } 196 | Err(e) => { 197 | log::error!("init PinSet proxy event handler > fail: {}", e); 198 | } 199 | }; 200 | } 201 | PinMode::Out => {} 202 | } 203 | 204 | Ok(JsValueFacade::Null) 205 | }) 206 | 207 | 208 | }) 209 | .method("setState", |_runtime, realm, instance_id, args| { 210 | // return prom 211 | 212 | if args.len() != 1 { 213 | return Err(JsError::new_str("setState expects a single Array arg.")); 214 | } 215 | if args[0].get_js_type() != JsValueType::Array { 216 | return Err(JsError::new_str("setState expects a single Array arg.")); 217 | } 218 | 219 | let mut states = vec![]; 220 | let ct = realm.get_array_length(&args[0])?; 221 | for x in 0..ct { 222 | let state_ref = realm.get_array_element(&args[0], x)?; 223 | if state_ref.get_js_type() != JsValueType::I32 { 224 | return Err(JsError::new_str("states array should be an array of Numbers")); 225 | } 226 | states.push(state_ref.to_i32() as u8); 227 | } 228 | 229 | wrap_prom(realm, instance_id, move |pin_set| { 230 | 231 | pin_set.set_state(states.as_slice()).map_err(|e| {JsError::new_string(e)})?; 232 | Ok(JsValueFacade::Null) 233 | 234 | }) 235 | 236 | }) 237 | .method("getState", |_runtime, realm, _instance_id, _args| { 238 | // todo return prom 239 | realm.create_null() 240 | }) 241 | .method("sequence", |_runtime, realm, instance_id, args| { 242 | // return prom 243 | 244 | if args.len() != 3 { 245 | return Err(JsError::new_str("sequence expects 3 args, (steps: Array>, pause_ms: number, repeats: Number)")); 246 | } 247 | if args[0].is_array() { 248 | return Err(JsError::new_str("sequence expects 3 args, (steps: Array>, pause_ms: number, repeats: Number)")); 249 | } 250 | if !args[1].is_i32() { 251 | return Err(JsError::new_str("sequence expects 3 args, (steps: Array>, pause_ms: number, repeats: Number)")); 252 | } 253 | if !args[2].is_i32() { 254 | return Err(JsError::new_str("sequence expects 3 args, (steps: Array>, pause_ms: number, repeats: Number)")); 255 | } 256 | 257 | let mut steps: Vec> = vec![]; 258 | 259 | let ct = realm.get_array_length(&args[0])?; 260 | for x in 0..ct { 261 | let step_arr = realm.get_array_element(&args[0], x)?; 262 | if step_arr.get_js_type() != JsValueType::Array { 263 | return Err(JsError::new_str("sequence expects 3 args, (steps: Array>, pause_ms: number, repeats: Number)")); 264 | } 265 | let mut step_vec = vec![]; 266 | 267 | for y in 0..realm.get_array_length(&step_arr)? { 268 | let v_ref = realm.get_array_element(&step_arr, y)?; 269 | if v_ref.get_js_type() != JsValueType::I32 { 270 | return Err(JsError::new_str("sequence expects 3 args, (steps: Array>, pause_ms: number, repeats: Number)")); 271 | } 272 | let v = v_ref.to_i32(); 273 | step_vec.push(v as u8); 274 | } 275 | 276 | steps.push(step_vec) 277 | } 278 | 279 | let step_delay =args[1].to_i32(); 280 | let repeats = args[2].to_i32(); 281 | 282 | wrap_prom(realm, instance_id, move |pin_set| { 283 | pin_set.sequence(steps, step_delay, repeats).map_err(|err| {JsError::new_string(err)})?; 284 | Ok(JsValueFacade::Null) 285 | }) 286 | 287 | }) 288 | .method("softPwm", |_runtime, realm, instance_id, args: &[QuickJsValueAdapter]| { 289 | 290 | if args.len() < 2 || !args[0].is_i32() || !(args[1].is_i32() || args[1].is_f64()) { 291 | return Err(JsError::new_str("softPwm2 expects 2 or 3 args, (duration: number, dutyCycle: number, pulseCount?: number)")); 292 | } 293 | 294 | let frequency = args[0].to_i32() as u64; 295 | let duty_cycle = if args[1].is_f64() { 296 | args[1].to_f64() 297 | } else { 298 | args[1].to_i32() as f64 299 | }; 300 | let pulse_count = if args[2].is_null_or_undefined() { 301 | 0_usize 302 | } else if args[2].is_f64() { 303 | args[2].to_f64() as usize 304 | } else { 305 | args[2].to_i32() as usize 306 | }; 307 | 308 | let receiver = PIN_SET_HANDLES.with(move |rc| { 309 | let handles = &mut *rc.borrow_mut(); 310 | let pin_set_handle = handles.get_mut(instance_id).expect("no such handle"); 311 | 312 | // stop if running 313 | if let Some(stopper) = pin_set_handle.pwm_stop_sender.take() { 314 | let _ = stopper.try_send(true); 315 | } 316 | 317 | // set new stopper 318 | let (sender, receiver) = sync_channel(1); 319 | let _ = pin_set_handle.pwm_stop_sender.replace(sender); 320 | receiver 321 | }); 322 | 323 | wrap_prom(realm, instance_id, move |pin_set| { 324 | pin_set.run_pwm_sequence(frequency, duty_cycle, pulse_count, receiver).map_err(|e| { JsError::new_string(e) })?; 325 | Ok(JsValueFacade::Null) 326 | }) 327 | 328 | }) 329 | .method("softPwmOff", |_runtime, realm, instance_id, _args| { 330 | PIN_SET_HANDLES.with(move |rc| { 331 | let handles = &mut *rc.borrow_mut(); 332 | let pin_set_handle = handles.get_mut(instance_id).expect("no such handle"); 333 | if let Some(stopper) = pin_set_handle.pwm_stop_sender.take() { 334 | let _ = stopper.try_send(true); 335 | } 336 | }); 337 | realm.create_null() 338 | }) 339 | .finalizer(|_runtime, _q_ctx, instance_id| { 340 | PIN_SET_HANDLES.with(|rc| { 341 | let handles = &mut *rc.borrow_mut(); 342 | handles.remove(&instance_id); 343 | }) 344 | }) 345 | ; 346 | let pinset_proxy = realm.install_proxy(pin_set_proxy_class, false)?; 347 | 348 | Ok(vec![("PinSet", pinset_proxy)]) 349 | } 350 | 351 | struct GpioModuleLoader {} 352 | 353 | impl NativeModuleLoader for GpioModuleLoader { 354 | fn has_module(&self, _realm: &QuickJsRealmAdapter, module_name: &str) -> bool { 355 | module_name.eq("greco://gpio") 356 | } 357 | 358 | fn get_module_export_names( 359 | &self, 360 | _realm: &QuickJsRealmAdapter, 361 | _module_name: &str, 362 | ) -> Vec<&str> { 363 | vec!["PinSet"] 364 | } 365 | 366 | fn get_module_exports( 367 | &self, 368 | realm: &QuickJsRealmAdapter, 369 | _module_name: &str, 370 | ) -> Vec<(&str, QuickJsValueAdapter)> { 371 | init_exports(realm).expect("init gpio exports failed") 372 | } 373 | } 374 | 375 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 376 | builder.native_module_loader(GpioModuleLoader {}) 377 | } 378 | -------------------------------------------------------------------------------- /src/modules/util/cache/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Cache module 2 | //! 3 | //! this module can be used as a machine local cache (caches are shared between runtimes (threads)) 4 | //! 5 | //! # Example 6 | //! 7 | //! ## cacheMod.getRegion(id: string, options?: object): greco.util.cache.Region 8 | //! 9 | //! Gets or initializes a region 10 | //! 11 | //! The options object may contain the following params (please note that if the region for the id already exists these are ignored) 12 | //! 13 | //! * items: number // default = 100.000 14 | //! maximum number of items to cache (when more items become present the least recently used will be removed even if withing its ttl) 15 | //! * idle: number // default = 3.600.000 (one hour) 16 | //! max idle (unused) time for an object in milliseconds 17 | //! * ttl: number // default = 86.400.000 (one day) 18 | //! max age for an object (the entry will be invalidated after this time even if recently used) 19 | //! 20 | //! # Example 21 | //! 22 | //! ```javascript 23 | //! import * as grecoCache from 'greco://cache'; 24 | //! const options = { 25 | //! items: 100000 26 | //! }; 27 | //! const cacheRegion = grecoCache.getRegion('my_cache_region_id', options); 28 | //! ``` 29 | //! 30 | //! ## cacheRegion.get(key: string, init: Function>): string | Promise 31 | //! gets or returns an item based on a key 32 | //! it may return the result (as string) directly or it may return a Promise 33 | //! if an item does not exist in the cache the init function is invoked 34 | //! 35 | //! ```javascript 36 | //! import * as grecoCache from 'greco://cache'; 37 | //! const cacheRegion = grecoCache.getRegion('my_cache_region_id'); 38 | //! export async function load(key) { 39 | //! return cacheRegion.get(key, async () => { 40 | //! return "largeLoadedThing_" + key; 41 | //! }); 42 | //! } 43 | //! ``` 44 | //! 45 | //! ## cacheRegion.remove(key: string): void 46 | //! removes an item from the cache 47 | //! 48 | 49 | use hirofa_utils::auto_id_map::AutoIdMap; 50 | use hirofa_utils::debug_mutex::DebugMutex; 51 | use lru::LruCache; 52 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 53 | use quickjs_runtime::jsutils::jsproxies::JsProxy; 54 | use quickjs_runtime::jsutils::modules::NativeModuleLoader; 55 | use quickjs_runtime::jsutils::JsError; 56 | use quickjs_runtime::quickjsrealmadapter::QuickJsRealmAdapter; 57 | use quickjs_runtime::quickjsvalueadapter::QuickJsValueAdapter; 58 | use quickjs_runtime::values::JsValueFacade; 59 | use std::cell::RefCell; 60 | use std::collections::HashMap; 61 | use std::ops::Sub; 62 | use std::sync::{Arc, Weak}; 63 | use std::thread; 64 | use std::time::{Duration, Instant}; 65 | 66 | struct CacheEntry { 67 | val: JsValueFacade, 68 | created: Instant, 69 | last_used: Instant, 70 | } 71 | 72 | struct CacheRegion { 73 | lru_cache: LruCache, 74 | ttl: Duration, 75 | max_idle: Duration, 76 | } 77 | 78 | impl CacheRegion { 79 | pub fn get(&mut self, key: &str) -> Option<&CacheEntry> { 80 | if let Some(ce) = self.lru_cache.get_mut(key) { 81 | ce.last_used = Instant::now(); 82 | } 83 | self.lru_cache.get(key) 84 | } 85 | pub fn remove(&mut self, key: &str) -> Option { 86 | self.lru_cache.pop(key) 87 | } 88 | pub fn put(&mut self, key: String, val: JsValueFacade) { 89 | let ce = CacheEntry { 90 | val, 91 | created: Instant::now(), 92 | last_used: Instant::now(), 93 | }; 94 | self.lru_cache.put(key, ce); 95 | } 96 | fn invalidate_stale(&mut self) { 97 | let min_last_used = Instant::now().sub(self.max_idle); 98 | let min_created = Instant::now().sub(self.ttl); 99 | while let Some(lru) = self.lru_cache.peek_lru() { 100 | if lru.1.last_used.lt(&min_last_used) || lru.1.created.lt(&min_created) { 101 | // invalidate 102 | let _ = self.lru_cache.pop_lru(); 103 | } else { 104 | // oldest was still valid, break of search 105 | break; 106 | } 107 | } 108 | } 109 | } 110 | 111 | struct ManagedCache { 112 | regions: HashMap<(String, String), Weak>>, 113 | } 114 | 115 | impl ManagedCache { 116 | fn new() -> Self { 117 | Self { 118 | regions: HashMap::new(), 119 | } 120 | } 121 | 122 | pub fn get_or_create_region( 123 | &mut self, 124 | realm_id: &str, 125 | cache_id: &str, 126 | max_idle: Duration, 127 | ttl: Duration, 128 | max_items: usize, 129 | ) -> Arc> { 130 | let key = (realm_id.to_string(), cache_id.to_string()); 131 | if let Some(weak) = self.regions.get(&key) { 132 | if let Some(arc) = weak.upgrade() { 133 | return arc; 134 | } 135 | } 136 | // new 137 | let region = CacheRegion { 138 | lru_cache: LruCache::new(std::num::NonZeroUsize::new(max_items).unwrap()), 139 | ttl, 140 | max_idle, 141 | }; 142 | let region_arc = Arc::new(DebugMutex::new(region, "region_mutex")); 143 | self.regions.insert(key, Arc::downgrade(®ion_arc)); 144 | region_arc 145 | } 146 | } 147 | 148 | fn cache_cleanup() { 149 | 150 | let mut to_clean = vec![]; 151 | { 152 | let lock: &mut ManagedCache = &mut CACHE.lock("cache_cleanup").unwrap(); 153 | let keys: Vec<(String, String)> = lock.regions.keys().cloned().collect(); 154 | for key in keys { 155 | let weak_opt = lock.regions.get(&key); 156 | if let Some(weak) = weak_opt { 157 | if let Some(cache_arc) = weak.upgrade() { 158 | to_clean.push((key, cache_arc.clone())); 159 | } else { 160 | lock.regions.remove(&key); 161 | } 162 | } else { 163 | lock.regions.remove(&key); 164 | } 165 | } 166 | } 167 | for (_key, cache_to_clean) in to_clean { 168 | let cache_lock = &mut *cache_to_clean.lock("cache_cleanup").unwrap(); 169 | 170 | cache_lock.invalidate_stale(); 171 | 172 | } 173 | } 174 | 175 | lazy_static! { 176 | static ref CACHE: Arc> = { 177 | // start cleanup thread 178 | thread::spawn(|| loop { 179 | thread::sleep(Duration::from_secs(30)); 180 | cache_cleanup(); 181 | }); 182 | Arc::new(DebugMutex::new(ManagedCache::new(), "CACHE")) 183 | 184 | }; 185 | } 186 | 187 | /* todo, reimpl 188 | CACHE > mutex > HashMap> 189 | 190 | Drop for CacheRegion > remove from CACHE HashMap 191 | 192 | getRegion > get or add to CACHE hashMap 193 | 194 | CacheRegion > RwLock? does LRU have a non mut get/peek? 195 | 196 | */ 197 | 198 | thread_local! { 199 | static CACHES: RefCell>>> = RefCell::new(AutoIdMap::new()); 200 | } 201 | 202 | fn with_cache_region R, R>(id: &usize, consumer: C) -> R { 203 | CACHES.with(|rc| { 204 | let caches = &mut *rc.borrow_mut(); 205 | let cache_mtx = caches.get(id).expect("invalid cache id"); 206 | let cache_locked = &mut *cache_mtx.lock("with_cache_region").unwrap(); 207 | consumer(cache_locked) 208 | }) 209 | } 210 | 211 | struct CacheModuleLoader { 212 | // 213 | } 214 | 215 | impl NativeModuleLoader for CacheModuleLoader { 216 | fn has_module(&self, _realm: &QuickJsRealmAdapter, module_name: &str) -> bool { 217 | module_name.eq("greco://cache") 218 | } 219 | 220 | fn get_module_export_names( 221 | &self, 222 | _realm: &QuickJsRealmAdapter, 223 | _module_name: &str, 224 | ) -> Vec<&str> { 225 | vec!["getRegion"] 226 | } 227 | 228 | fn get_module_exports( 229 | &self, 230 | realm: &QuickJsRealmAdapter, 231 | _module_name: &str, 232 | ) -> Vec<(&str, QuickJsValueAdapter)> { 233 | init_region_proxy(realm).expect("init cache region failed"); 234 | 235 | init_exports(realm).expect("init cache exports failed") 236 | } 237 | } 238 | 239 | fn cache_add( 240 | realm: &QuickJsRealmAdapter, 241 | key: String, 242 | value: &QuickJsValueAdapter, 243 | region: &mut CacheRegion, 244 | ) -> Result<(), JsError> { 245 | if value.is_string() || value.is_i32() || value.is_f64() || value.is_bool() { 246 | let jsvf = realm.to_js_value_facade(value)?; 247 | 248 | region.put(key, jsvf); 249 | 250 | Ok(()) 251 | } else { 252 | Err(JsError::new_str("Only cache primitives")) 253 | } 254 | } 255 | 256 | fn init_region_proxy(realm: &QuickJsRealmAdapter) -> Result<(), JsError> { 257 | let proxy = JsProxy::new() 258 | .namespace(&["greco", "util", "cache"]) 259 | .name("Region") 260 | .method("get", |_rt, realm, instance_id, args| { 261 | if args.len() < 2 || !args[0].is_string() || !args[1].is_function() { 262 | return Err(JsError::new_str( 263 | "get requires two arguments, key:string and init:function", 264 | )); 265 | } 266 | 267 | let instance_id = *instance_id; 268 | 269 | let key = args[0].to_string()?; 270 | 271 | with_cache_region(&instance_id, move |cache_region| { 272 | let entry_opt = cache_region.get(key.as_str()); 273 | if let Some(entry) = entry_opt { 274 | let jsvf = &entry.val; 275 | match jsvf { 276 | JsValueFacade::I32 { val } => realm.create_i32(*val), 277 | JsValueFacade::F64 { val } => realm.create_f64(*val), 278 | JsValueFacade::String { val } => realm.create_string(val), 279 | JsValueFacade::Boolean { val } => realm.create_boolean(*val), 280 | _ => Err(JsError::new_str("unexpected cached jsvf type")), 281 | } 282 | } else { 283 | let init_func = &args[1]; 284 | 285 | let init_result = if args.len() > 2 { 286 | realm.invoke_function(None, init_func, &[&args[0], &args[2]])? 287 | } else { 288 | realm.invoke_function(None, init_func, &[&args[0]])? 289 | }; 290 | 291 | if init_result.is_promise() { 292 | let then = realm.create_function( 293 | "cache_add_func", 294 | move |realm, _this, args| { 295 | // cache args 0 296 | let key_clone = key.clone(); 297 | with_cache_region(&instance_id, |cache_region2| { 298 | cache_add(realm, key_clone, &args[0], cache_region2) 299 | })?; 300 | 301 | realm.create_null() 302 | }, 303 | 1, 304 | )?; 305 | realm.add_promise_reactions(&init_result, Some(then), None, None)?; 306 | } else { 307 | cache_add(realm, key, &init_result, cache_region)?; 308 | } 309 | Ok(init_result) 310 | } 311 | }) 312 | }) 313 | .method("put", |_rt, realm, instance_id, args| { 314 | if args.len() != 2 315 | || !args[0].is_string() 316 | || !(args[1].is_string() 317 | || args[1].is_i32() 318 | || args[1].is_bool() 319 | || args[1].is_f64()) 320 | { 321 | return Err(JsError::new_str( 322 | "put requires two arguments, key:string and value:string|boolean|i32|f64", 323 | )); 324 | } 325 | 326 | let key = args[0].to_string()?; 327 | let val = realm.to_js_value_facade(&args[1])?; 328 | 329 | with_cache_region(instance_id, move |cache_region| { 330 | cache_region.put(key, val); 331 | }); 332 | realm.create_null() 333 | }) 334 | .method("remove", |_rt, realm, instance_id, args| { 335 | if args.len() != 1 || !args[0].is_string() { 336 | return Err(JsError::new_str( 337 | "remove requires one arguments, key:string", 338 | )); 339 | } 340 | 341 | let key = args[0].to_string()?; 342 | 343 | with_cache_region(instance_id, |region| { 344 | region.remove(key.as_str()); 345 | }); 346 | realm.create_null() 347 | }) 348 | .finalizer(|_rt, _realm, instance_id| { 349 | // 350 | CACHES.with(|rc| { 351 | let caches = &mut *rc.borrow_mut(); 352 | let _ = caches.remove(&instance_id); 353 | }) 354 | }); 355 | 356 | realm.install_proxy(proxy, false)?; 357 | Ok(()) 358 | } 359 | 360 | fn init_exports( 361 | realm: &QuickJsRealmAdapter, 362 | ) -> Result, JsError> { 363 | let cache_region_function = realm.create_function( 364 | "getRegion", 365 | |realm, _this, args| { 366 | if args.is_empty() || !args[0].is_string() || (args.len() > 1 && !args[1].is_object()) { 367 | return Err(JsError::new_str( 368 | "getRegion requires one or two arguments, id:string and init: object", 369 | )); 370 | } 371 | 372 | let cache = &mut *CACHE.lock("getRegion").unwrap(); 373 | 374 | let items_ref = realm.get_object_property(&args[1], "items")?; 375 | let items = if items_ref.is_i32() { 376 | items_ref.to_i32() as usize 377 | } else { 378 | 100000 379 | }; 380 | let idle_ref = realm.get_object_property(&args[1], "idle")?; 381 | let ttl_ref = realm.get_object_property(&args[1], "ttl")?; 382 | 383 | let cache_id = args[0].to_string()?; 384 | let idle = Duration::from_secs(if idle_ref.is_i32() { 385 | idle_ref.to_i32() as u64 386 | } else { 387 | 3600 388 | }); 389 | let ttl = Duration::from_secs(if ttl_ref.is_i32() { 390 | ttl_ref.to_i32() as u64 391 | } else { 392 | 86400 393 | }); 394 | 395 | let region = cache.get_or_create_region( 396 | realm.get_realm_id(), 397 | cache_id.as_str(), 398 | idle, 399 | ttl, 400 | items, 401 | ); 402 | 403 | let instance_id = CACHES.with(|rc| { 404 | let caches = &mut *rc.borrow_mut(); 405 | caches.insert(region) 406 | }); 407 | 408 | realm.instantiate_proxy_with_id(&["greco", "util", "cache"], "Region", instance_id) 409 | }, 410 | 1, 411 | )?; 412 | 413 | Ok(vec![("getRegion", cache_region_function)]) 414 | } 415 | 416 | pub(crate) fn init(builder: QuickJsRuntimeBuilder) -> QuickJsRuntimeBuilder { 417 | // todo 418 | 419 | // add greco://cache module (machine local cache) 420 | // config per region, every region is a LRUCache 421 | builder.native_module_loader(CacheModuleLoader {}) 422 | } 423 | 424 | #[cfg(test)] 425 | pub mod tests { 426 | use crate::init_greco_rt; 427 | 428 | use quickjs_runtime::builder::QuickJsRuntimeBuilder; 429 | use quickjs_runtime::jsutils::Script; 430 | use quickjs_runtime::values::JsValueFacade; 431 | use std::panic; 432 | 433 | #[tokio::test] 434 | async fn my_test() { 435 | /* 436 | panic::set_hook(Box::new(|panic_info| { 437 | let backtrace = Backtrace::new(); 438 | println!("thread panic occurred: {panic_info}\nbacktrace: {backtrace:?}"); 439 | log::error!( 440 | "thread panic occurred: {}\nbacktrace: {:?}", 441 | panic_info, 442 | backtrace 443 | ); 444 | })); 445 | 446 | simple_logging::log_to_file("greco_rt.log", LevelFilter::Debug) 447 | .ok() 448 | .unwrap(); 449 | */ 450 | let mut builder = QuickJsRuntimeBuilder::new(); 451 | 452 | builder = init_greco_rt(builder); 453 | 454 | let rt = builder.build(); 455 | 456 | let res = rt 457 | .eval( 458 | None, 459 | Script::new( 460 | "test_cache.js", 461 | r#" 462 | 463 | async function initItem(key) { 464 | return await new Promise((res, rej) => { 465 | setTimeout(() => {res("abc " + key);}, 1000); 466 | }); 467 | } 468 | 469 | async function testCache(){ 470 | let grecoCache = await import("greco://cache"); 471 | const options = { 472 | items: 100000 473 | }; 474 | const cacheRegion = grecoCache.getRegion('my_cache_region_id', options); 475 | 476 | const t1 = new Date(); 477 | 478 | const key = "123" 479 | 480 | const a = await cacheRegion.get(key, initItem); 481 | 482 | const t2 = new Date(); 483 | 484 | const b = await cacheRegion.get(key, initItem); 485 | 486 | const t3 = new Date(); 487 | 488 | const c = await cacheRegion.get(key, initItem); 489 | 490 | const t4 = new Date(); 491 | 492 | const d = await cacheRegion.get(key, initItem); 493 | 494 | for (let x = 0; x < 1000; x++) { 495 | let xRes = await cacheRegion.get(key, initItem); 496 | } 497 | 498 | const t5 = new Date(); 499 | 500 | 501 | 502 | return `s = ${t1.getTime()} 503 | a = ${a} @ t2 after ${t2.getTime() - t1.getTime()}ms 504 | b = ${b} @ t3 after ${t3.getTime() - t2.getTime()}ms 505 | c = ${c} @ t4 after ${t4.getTime() - t3.getTime()}ms 506 | d = ${d} @ t5 after ${t5.getTime() - t4.getTime()}ms 507 | `; 508 | 509 | } 510 | 511 | testCache() 512 | 513 | "#, 514 | ), 515 | ) 516 | .await 517 | .expect("script failed"); 518 | 519 | match res { 520 | JsValueFacade::JsPromise { cached_promise } => { 521 | let prom_res = cached_promise 522 | .get_promise_result() 523 | .await 524 | .expect("prom timed out"); 525 | match prom_res { 526 | Ok(r) => { 527 | println!("prom resolved to {r:?}"); 528 | } 529 | Err(e) => { 530 | println!("prom errored to {e:?}"); 531 | } 532 | } 533 | } 534 | _ => { 535 | panic!("that was not a promise...") 536 | } 537 | } 538 | 539 | //std::thread::sleep(Duration::from_secs(35)); 540 | } 541 | } 542 | --------------------------------------------------------------------------------