├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── build.mjs ├── common.gypi ├── deps ├── asar.js ├── filename_rules.gypi ├── js2c.py ├── mocha.js ├── ninja │ ├── ninja │ ├── ninja-linux32 │ ├── ninja-linux64 │ ├── ninja-mac │ └── ninja.exe └── node.def ├── src ├── asar_archive.js ├── asar_monkey_patch.js ├── bootstrap.js ├── main.cc ├── node_integration.cc ├── node_integration.h ├── node_integration_linux.cc ├── node_integration_linux.h ├── node_integration_mac.h ├── node_integration_mac.mm ├── node_integration_win.cc ├── node_integration_win.h ├── pickle.js ├── yode.cc ├── yode.exe.manifest ├── yode.h ├── yode.ico ├── yode.rc ├── yode_linux.cc ├── yode_mac.mm └── yode_win.cc ├── test ├── asar_exit_123 │ └── index.js ├── asar_fs_async │ └── index.js ├── asar_fs_promise │ └── index.js ├── asar_fs_realpath_dir │ ├── dir │ │ └── file │ └── index.js ├── asar_print_filename │ └── index.js ├── mac.js └── main.js └── yode.gyp /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: yode 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: >- 8 | ${{ 9 | (matrix.os == 'mac' && matrix.arch == 'arm64') && 10 | 'macos-15' || 11 | (fromJson('{"linux":"ubuntu-22.04","mac":"macos-13","win":"windows-2022"}')[matrix.os]) 12 | }} 13 | continue-on-error: false 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [linux, mac, win] 19 | arch: [x64] 20 | include: 21 | - os: mac 22 | arch: arm64 23 | 24 | steps: 25 | - name: Install Linux Dependencies 26 | if: matrix.os == 'linux' 27 | run: | 28 | sudo apt update 29 | sudo apt-get install -y libgtk-3-dev 30 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 31 | 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | 35 | - uses: actions/setup-python@v3 36 | with: 37 | python-version: '3.11' 38 | 39 | - name: CCache 40 | uses: hendrikmuhs/ccache-action@v1.2 41 | with: 42 | max-size: '2G' 43 | key: ccache-${{ matrix.os }}-${{ matrix.arch }} 44 | 45 | - name: Build 46 | env: 47 | CCACHE_SLOPPINESS: time_macros 48 | run: npx zx build.mjs ${{ matrix.arch }} --verbose 49 | 50 | - name: Test 51 | if: matrix.arch == runner.arch 52 | env: 53 | DISPLAY: ':99.0' 54 | run: out/Release/yode test/main.js 55 | 56 | - name: Upload Binary Files 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: yode-${{ matrix.os }}-${{ matrix.arch }} 60 | path: out/Release/*.zip 61 | retention-days: 1 62 | 63 | release: 64 | if: startsWith(github.ref, 'refs/tags/') 65 | needs: [build] 66 | runs-on: ubuntu-latest 67 | 68 | steps: 69 | - name: Download Files 70 | uses: actions/download-artifact@v4 71 | with: 72 | merge-multiple: true 73 | 74 | - name: Release 75 | uses: softprops/action-gh-release@v2 76 | with: 77 | draft: true 78 | name: Yode ${{ github.ref_name }} 79 | body: '## Changelog' 80 | files: '*.zip' 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /config.gypi 3 | /out 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "node"] 2 | path = node 3 | url = https://github.com/yue/node.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Cheng Zhao 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yode 2 | 3 | Yode is a fork of Node.js that replaces its event loop with GUI message loop, 4 | it is designed to be used together with [the Yue library](http://libyue.com). 5 | 6 | ## Changes to Node.js 7 | 8 | * The event loop is replaced with native GUI message loops: 9 | * On Linux it is `GTK+` event loop; 10 | * On macOS it is `Cocoa` run loop; 11 | * On Windows it is `Win32` message loop. 12 | * When the executable is appended with ASAR archive, Yode will try to start with 13 | app inside the archive. 14 | * The process will not automatically quit when there is no work, you have to 15 | call the native APIs to quit the GUI message loop. 16 | * The process will quit when **BOTH** the GUI message loop and Node.js event 17 | loop have quit. So if there are still Node.js requests pending, the process 18 | will wait until all of them have finished. 19 | * There is a new `process.versions.yode` property added. 20 | * The `process.stdin` is not supposed to work. 21 | * On Windows the executable uses `WINDOWS` subsystem instead of `CONSOLE` 22 | subsystem, so unlike Node.js there is no console attached and REPL won't 23 | work. 24 | 25 | ## Usage 26 | 27 | The prebuilt binaries can be found in the Releases page, modules installed by 28 | `npm` can be used directly in Yode. 29 | 30 | To package your Node.js project with Yode, you should use [yackage][yackage]. 31 | 32 | Note that it is strong recommended to install the official Node.js with the 33 | same version of Yode, otherwise native modules installed by `npm` may not work 34 | correctly in Yode. 35 | 36 | ## Build 37 | 38 | ```bash 39 | $ npx zx ./build.mjs [x64|arm64] 40 | ``` 41 | 42 | ## License 43 | 44 | The MIT license. 45 | 46 | [yackage]: https://github.com/yue/yackage 47 | -------------------------------------------------------------------------------- /build.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | $.verbose = argv.verbose 4 | 5 | const hostArch = os.arch() 6 | 7 | // Parse args. 8 | let buildType = 'Release' 9 | let targetArch = hostArch 10 | for (const arg of argv._) { 11 | if (arg in ['Debug', 'Release']) 12 | buildType = arg 13 | else 14 | targetArch = arg 15 | } 16 | 17 | // Current version. 18 | const version = (await $`git describe --always --tags`).valueOf() 19 | 20 | // Sync submodule. 21 | await $`git submodule sync --recursive` 22 | await $`git submodule update --init --recursive` 23 | 24 | // Find out where VS is installed. 25 | if (process.platform == 'win32') { 26 | const vswhere = `${process.env['ProgramFiles(x86)']}/Microsoft Visual Studio/Installer/vswhere.exe` 27 | const result = JSON.parse(await $`${vswhere} -format json`) 28 | if (result.length == 0) 29 | throw new Error('Unable to find Visual Studio') 30 | const vs = result[0] 31 | process.env.GYP_MSVS_VERSION = vs.displayName.match(/(\d+)$/)[1] 32 | process.env.GYP_MSVS_OVERRIDE_PATH = vs.installationPath 33 | } 34 | 35 | // Required for cross compilation on macOS. 36 | if (hostArch != targetArch && process.platform == 'darwin') { 37 | process.env.GYP_CROSSCOMPILE = '1' 38 | Object.assign(process.env, { 39 | CC: `cc -arch ${targetArch}`, 40 | CXX: `c++ -arch ${targetArch}`, 41 | CC_target: `cc -arch ${targetArch}`, 42 | CXX_target: `c++ -arch ${targetArch}`, 43 | CC_host: 'cc -arch x86_64', 44 | CXX_host: 'c++ -arch x86_64', 45 | }) 46 | } 47 | 48 | // Handle ccache. 49 | try { 50 | const ccache = await which('ccache') 51 | Object.assign(process.env, { 52 | 'CC_wrapper': ccache, 53 | 'CXX_wrapper': ccache, 54 | 'CC.host_wrapper': ccache, 55 | 'CXX.host_wrapper': ccache, 56 | }) 57 | console.log('Using ccache located at', ccache) 58 | } catch {} 59 | 60 | // Find Python 3 61 | let python = 'python' 62 | for (const p of ['python', 'python3']) { 63 | try { 64 | const version = await $`${p} --version` 65 | if (version.startsWith('Python 3')) { 66 | python = p 67 | break 68 | } 69 | } catch (error) {} 70 | } 71 | 72 | // Generate some dynamic gyp files. 73 | const configureArgs = [ 74 | '--with-intl=small-icu', 75 | '--without-node-code-cache', 76 | '--openssl-no-asm', 77 | `--dest-cpu=${targetArch}`, 78 | ] 79 | await $({cwd: 'node'})`${python} configure.py ${configureArgs}` 80 | 81 | // Update the build configuration. 82 | const config = { 83 | variables: { 84 | python, 85 | target_arch: targetArch, 86 | host_arch: hostArch, 87 | want_separate_host_toolset: hostArch == targetArch ? 0 : 1, 88 | } 89 | } 90 | if (process.platform == 'darwin') { 91 | // Set SDK version to the latest installed. 92 | const sdks = await $`xcodebuild -showsdks` 93 | const SDKROOT = sdks.stdout.match(/-sdk (macosx\d+\.\d+)/)[1] 94 | config.xcode_settings = {SDKROOT} 95 | } 96 | // Copy fields from config.gypi of node. 97 | const configGypiPath = fs.readFileSync(path.join(__dirname, 'node', 'config.gypi')).toString() 98 | const configGypi = JSON.parse(configGypiPath.split('\n').slice(1).join('\n').replace(/'/g, '"')) 99 | for (const key in configGypi.variables) { 100 | if (!(key in config.variables)) 101 | config.variables[key] = configGypi.variables[key] 102 | } 103 | // Map node_library_files from config.gypi. 104 | config.variables.node_library_files = configGypi.variables.node_library_files.map(l => 'node/' + l) 105 | // Write our own config.gypi file. 106 | fs.writeFileSync(`${__dirname}/config.gypi`, JSON.stringify(config, null, ' ')) 107 | 108 | await $`${python} node/tools/gyp/gyp_main.py yode.gyp --no-parallel -f ninja -Dbuild_type=${buildType} -Iconfig.gypi -Icommon.gypi --depth .` 109 | 110 | // Build. 111 | const ninja = process.platform == 'win32' ? 'deps/ninja/ninja.exe' 112 | : 'deps/ninja/ninja' 113 | const jobs = argv.j ?? os.cpus().length 114 | await $`${ninja} -j ${jobs} -C out/${buildType} yode` 115 | 116 | if (process.platform == 'linux') 117 | await $`strip out/${buildType}/yode` 118 | 119 | // Remove old zip. 120 | const files = fs.readdirSync(`out/${buildType}`) 121 | for (let f of files) { 122 | if (f.endsWith('.zip')) 123 | fs.unlinkSync(`out/${buildType}/${f}`) 124 | } 125 | 126 | // Create zip. 127 | const distname = `out/${buildType}/yode-${version}-${process.platform}-${targetArch}` 128 | const filename = process.platform == 'win32' ? 'yode.exe' : 'yode' 129 | await fs.emptyDir('dist') 130 | await fs.copy('node/LICENSE', 'dist/LICENSE') 131 | await fs.copy(`out/${buildType}/${filename}`, `dist/${filename}`) 132 | await $`${python} -c "import shutil; shutil.make_archive('${distname}', 'zip', 'dist')"` 133 | await fs.remove('dist') 134 | -------------------------------------------------------------------------------- /common.gypi: -------------------------------------------------------------------------------- 1 | { 2 | 'includes': [ 3 | 'node/common.gypi', 4 | ], 5 | 'variables': { 6 | 'component': 'static_library', 7 | 'icu_gyp_path': 'node/tools/icu/icu-generic.gyp', 8 | }, 9 | 'target_defaults': { 10 | 'includes': [ 11 | 'deps/filename_rules.gypi', 12 | ], 13 | 'include_dirs': [ 14 | 'node/deps/v8/include', 15 | ], 16 | 'target_conditions': [ 17 | ['_target_name=="libnode"', { 18 | 'defines': [ 19 | 'DISABLE_SINGLE_EXECUTABLE_APPLICATION', 20 | ], 21 | }], 22 | ['_target_name=="libnode" and OS=="win"', { 23 | # Force loading all objects of node, otherwise some built-in modules 24 | # won't load. 25 | 'sources': [ 26 | 'deps/node.def', 27 | ], 28 | 'defines': [ 29 | # We want to export Node's symbols but do not wish to change its 30 | # vc runtime settings. 31 | 'NODE_SHARED_MODE', 32 | # ICU is built as static library and this has to be defined for its 33 | # users on Windows. 34 | 'U_STATIC_IMPLEMENTATION=1', 35 | ], 36 | }], 37 | ['_target_name in ["v8_base_without_compiler", "v8_initializers"] and OS=="win"', { 38 | # Required for avoiding LINK error: 39 | # fatal error LNK1248: image size exceeds maximum allowable size 40 | 'msvs_shard': 4, 41 | }], 42 | ['_target_name in ["genrb", "genccode"] or _target_name.startswith("libnode") or _target_name.startswith("icu")', { 43 | # Somehow Node's gyp files are not adding the include dirs. 44 | 'include_dirs': [ 45 | 'node/deps/icu-small/source/common', 46 | 'node/deps/icu-small/source/i18n', 47 | 'node/deps/icu-small/source/tools/toolutil', 48 | ], 49 | }], 50 | ['_target_name in ["libuv", "http_parser", "openssl", "openssl-cli", "cares", "libnode", "nghttp2", "zlib", "mksnapshot", "genrb", "genccode", "simdutf"] or _target_name.startswith("v8") or _target_name.startswith("icu") or _target_name.startswith("node") or _target_name.startswith("torque")', { 51 | # Suppress all the warnings in Node. 52 | 'msvs_settings': { 53 | 'VCCLCompilerTool': { 54 | 'WarningLevel': 0, 55 | }, 56 | }, 57 | 'msvs_disabled_warnings': [ 58 | 4003, 59 | 4146, 60 | 4244, 61 | 4251, 62 | 4996, 63 | ], 64 | 'xcode_settings': { 65 | 'WARNING_CFLAGS': [ 66 | '-Wno-deprecated-declarations', 67 | '-Wno-undefined-var-template', 68 | '-Wno-switch', 69 | '-Wno-unused-function', 70 | '-Wno-sign-compare', 71 | '-Wno-implicit-function-declaration', 72 | '-Wno-inconsistent-missing-override', 73 | ], 74 | 'WARNING_CFLAGS!': [ 75 | '-W', 76 | '-Wall', 77 | ], 78 | }, 79 | 'cflags': [ 80 | '-Wno-deprecated-declarations', 81 | '-Wno-switch', 82 | '-Wno-unused-function', 83 | '-Wno-sign-compare', 84 | '-Wno-unused-but-set-variable', 85 | '-Wno-maybe-uninitialized', 86 | '-Wno-inconsistent-missing-override', 87 | ], 88 | 'cflags_c': [ 89 | '-Wno-deprecated-non-prototype', 90 | '-Wno-implicit-function-declaration', 91 | ], 92 | 'cflags!': [ 93 | '-Wall', 94 | '-Wextra', 95 | ], 96 | }], 97 | ], 98 | }, 99 | } 100 | -------------------------------------------------------------------------------- /deps/asar.js: -------------------------------------------------------------------------------- 1 | // @electron/asar@3.2.3 2 | // 3 | // Copyright (c) 2014 GitHub Inc. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | // 24 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).asar=t()}}((function(){return function t(e,r,n){function i(o,a){if(!r[o]){if(!e[o]){var c="function"==typeof require&&require;if(!a&&c)return c(o,!0);if(s)return s(o,!0);var h=new Error("Cannot find module '"+o+"'");throw h.code="MODULE_NOT_FOUND",h}var u=r[o]={exports:{}};e[o][0].call(u.exports,(function(t){return i(e[o][1][t]||t)}),u,u.exports,t,e,r,n)}return r[o].exports}for(var s="function"==typeof require&&require,o=0;ot.startsWith(e)))}e.exports.createPackage=async function(t,r){return e.exports.createPackageWithOptions(t,r,{})},e.exports.createPackageWithOptions=async function(t,r,n){const i=n.globOptions?n.globOptions:{};i.dot=void 0===n.dot||n.dot;const s=t+(n.pattern?n.pattern:"/**/*"),[o,a]=await c(s,i);return e.exports.createPackageFromFiles(t,r,o,a,n)},e.exports.createPackageFromFiles=async function(t,e,r,u,f){null==u&&(u={}),null==f&&(f={}),t=i.normalize(t),e=i.normalize(e),r=r.map((function(t){return i.normalize(t)}));const l=new o(t),p=[],d=[];let y=[];if(f.ordering){const e=(await n.readFile(f.ordering)).toString().split("\n").map((t=>(t.includes(":")&&(t=t.split(":").pop()),(t=t.trim()).startsWith("/")&&(t=t.slice(1)),t))),s=[];for(const r of e){const e=r.split(i.sep);let n=t;for(const t of e)n=i.join(n,t),s.push(n)}let o=0;const a=r.length;for(const t of s)!y.includes(t)&&r.includes(t)&&y.push(t);for(const t of r)y.includes(t)||(y.push(t),o+=1);console.log(`Ordering file has ${(a-o)/a*100}% coverage.`)}else y=r;const m=y.slice(),g=async function(r){return r?(await async function(e){u[e]||(u[e]=await c.determineFileType(e));const r=u[e];let n;switch(r.type){case"directory":n=!!f.unpackDir&&h(i.relative(t,e),f.unpackDir,d),l.insertDirectory(e,n);break;case"file":return n=!1,f.unpack&&(n=s(e,f.unpack,{matchBase:!0})),!n&&f.unpackDir&&(n=h(i.relative(t,i.dirname(e)),f.unpackDir,d)),p.push({filename:e,unpack:n}),l.insertFile(e,n,r,f);case"link":l.insertLink(e)}return Promise.resolve()}(r),g(m.shift())):async function(){return await n.mkdirp(i.dirname(e)),a.writeFilesystem(e,l,p,u)}()};return g(m.shift())},e.exports.statFile=function(t,e,r){return a.readFilesystemSync(t).getFile(e,r)},e.exports.getRawHeader=function(t){return a.readArchiveHeaderSync(t)},e.exports.listPackage=function(t,e){return a.readFilesystemSync(t).listFiles(e)},e.exports.extractFile=function(t,e){const r=a.readFilesystemSync(t);return a.readFileSync(r,e,r.getFile(e))},e.exports.extractAll=function(t,e){const r=a.readFilesystemSync(t),s=r.listFiles(),o="win32"===process.platform;n.mkdirpSync(e);for(const t of s){const s=t.substr(1),c=i.join(e,s),h=r.getFile(s,o);if(h.files)n.mkdirpSync(c);else if(h.link){const t=i.dirname(i.join(e,h.link)),r=i.dirname(c),s=i.relative(r,t);try{n.unlinkSync(c)}catch{}const o=i.join(s,i.basename(h.link));n.symlinkSync(o,c)}else{const t=a.readFileSync(r,s,h);n.writeFileSync(c,t),h.executable&&n.chmodSync(c,"755")}}},e.exports.uncache=function(t){return a.uncacheFilesystem(t)},e.exports.uncacheAll=function(){a.uncacheAll()}},{"./crawlfs":2,"./disk":3,"./filesystem":4,"./wrapped-fs":6,minimatch:20,path:void 0}],2:[function(t,e,r){"use strict";const{promisify:n}=t("util"),i=t("./wrapped-fs"),s=n(t("glob"));async function o(t){const e=await i.lstat(t);return e.isFile()?{type:"file",stat:e}:e.isDirectory()?{type:"directory",stat:e}:e.isSymbolicLink()?{type:"link",stat:e}:void 0}e.exports=async function(t,e){const r={},n=await s(t,e),i=await Promise.all(n.map((async t=>[t,await o(t)]))),a=[];return[i.map((([t,e])=>(e&&(r[t]=e,"link"===e.type&&a.push(t)),t))).filter((t=>{const e=a.findIndex((e=>t===e));return a.every(((r,n)=>n===e||!t.startsWith(r)))})),r]},e.exports.determineFileType=o},{"./wrapped-fs":6,glob:15,util:void 0}],3:[function(t,e,r){"use strict";const n=t("./wrapped-fs"),i=t("path"),s=t("chromium-pickle-js"),o=t("./filesystem");let a={};async function c(t,e,r){const s=i.join(e,r),o=i.join(t,r),[a,c]=await Promise.all([n.readFile(s),n.stat(s),n.mkdirp(i.dirname(o))]);return n.writeFile(o,a,{mode:c.mode})}async function h(t,e,r){return new Promise(((i,s)=>{const o=n.createReadStream(r?r.path:t);o.pipe(e,{end:!1}),o.on("error",s),o.on("end",(()=>i()))}))}e.exports.writeFilesystem=async function(t,e,r,o){const a=s.createEmpty();a.writeString(JSON.stringify(e.header));const u=a.toBuffer(),f=s.createEmpty();f.writeUInt32(u.length);const l=f.toBuffer(),p=n.createWriteStream(t);return await new Promise(((t,e)=>(p.on("error",e),p.write(l),p.write(u,(()=>t()))))),async function(t,e,r,n,s){for(const o of n)if(o.unpack){const r=i.relative(e.src,o.filename);await c(`${t}.unpacked`,e.src,r)}else await h(o.filename,r,s[o.filename].transformed);return r.end()}(t,e,p,r,o)},e.exports.readArchiveHeaderSync=function(t){const e=n.openSync(t,"r");let r,i;try{const t=Buffer.alloc(8);if(8!==n.readSync(e,t,0,8,null))throw new Error("Unable to read header size");if(r=s.createFromBuffer(t).createIterator().readUInt32(),i=Buffer.alloc(r),n.readSync(e,i,0,r,null)!==r)throw new Error("Unable to read header")}finally{n.closeSync(e)}const o=s.createFromBuffer(i).createIterator().readString();return{headerString:o,header:JSON.parse(o),headerSize:r}},e.exports.readFilesystemSync=function(t){if(!a[t]){const e=this.readArchiveHeaderSync(t),r=new o(t);r.header=e.header,r.headerSize=e.headerSize,a[t]=r}return a[t]},e.exports.uncacheFilesystem=function(t){return!!a[t]&&(a[t]=void 0,!0)},e.exports.uncacheAll=function(){a={}},e.exports.readFileSync=function(t,e,r){let s=Buffer.alloc(r.size);if(r.size<=0)return s;if(r.unpacked)s=n.readFileSync(i.join(`${t.src}.unpacked`,e));else{const e=n.openSync(t.src,"r");try{const i=8+t.headerSize+parseInt(r.offset);n.readSync(e,s,0,r.size,i)}finally{n.closeSync(e)}}return s}},{"./filesystem":4,"./wrapped-fs":6,"chromium-pickle-js":9,path:void 0}],4:[function(t,e,r){"use strict";const n=t("./wrapped-fs"),i=t("os"),s=t("path"),{promisify:o}=t("util"),a=t("stream"),c=t("./integrity"),h=o(a.pipeline);e.exports=class{constructor(t){this.src=s.resolve(t),this.header={files:{}},this.offset=BigInt(0)}searchNodeFromDirectory(t){let e=this.header;const r=t.split(s.sep);for(const t of r)"."!==t&&(e.files[t]||(e.files[t]={files:{}}),e=e.files[t]);return e}searchNodeFromPath(t){if(!(t=s.relative(this.src,t)))return this.header;const e=s.basename(t),r=this.searchNodeFromDirectory(s.dirname(t));return null==r.files&&(r.files={}),null==r.files[e]&&(r.files[e]={}),r.files[e]}insertDirectory(t,e){const r=this.searchNodeFromPath(t);return e&&(r.unpacked=e),r.files={},r.files}async insertFile(t,e,r,o){const a=this.searchNodeFromPath(s.dirname(t)),u=this.searchNodeFromPath(t);if(e||a.unpacked)return u.size=r.stat.size,u.unpacked=!0,u.integrity=await c(t),Promise.resolve();let f;const l=o.transform&&o.transform(t);if(l){const e=await n.mkdtemp(s.join(i.tmpdir(),"asar-")),o=s.join(e,s.basename(t)),a=n.createWriteStream(o),c=n.createReadStream(t);await h(c,l,a),r.transformed={path:o,stat:await n.lstat(o)},f=r.transformed.stat.size}else f=r.stat.size;if(f>4294967295)throw new Error(`${t}: file size can not be larger than 4.2GB`);u.size=f,u.offset=this.offset.toString(),u.integrity=await c(t),"win32"!==process.platform&&64&r.stat.mode&&(u.executable=!0),this.offset+=BigInt(f)}insertLink(t){const e=s.relative(n.realpathSync(this.src),n.realpathSync(t));if(".."===e.substr(0,2))throw new Error(`${t}: file "${e}" links out of the package`);return this.searchNodeFromPath(t).link=e,e}listFiles(t){const e=[],r=function(n,i){if(i.files)for(const[o,a]of Object.entries(i.files)){const i=s.join(n,o),c=a.unpacked?"unpack":"pack ";e.push(t&&t.isPack?`${c} : ${i}`:i),r(i,a)}};return r("/",this.header),e}getNode(t){const e=this.searchNodeFromDirectory(s.dirname(t)),r=s.basename(t);return r?e.files[r]:e}getFile(t,e){e=void 0===e||e;const r=this.getNode(t);return r.link&&e?this.getFile(r.link):r}}},{"./integrity":5,"./wrapped-fs":6,os:void 0,path:void 0,stream:void 0,util:void 0}],5:[function(t,e,r){const n=t("crypto"),i=t("fs"),s=t("stream"),{promisify:o}=t("util"),a="SHA256",c=4194304,h=o(s.pipeline);function u(t){return n.createHash(a).update(t).digest("hex")}e.exports=async function(t){const e=n.createHash(a),r=[];let o=0,f=[];return await h(i.createReadStream(t),new s.PassThrough({decodeStrings:!1,transform(t,n,i){e.update(t),function t(e){const n=Math.min(c-o,e.byteLength);o+=n,f.push(e.slice(0,n)),o===c&&(r.push(u(Buffer.concat(f))),f=[],o=0),nn.promises.mkdir(t,{recursive:!0}),s.mkdirpSync=t=>n.mkdirSync(t,{recursive:!0}),e.exports=s},{fs:void 0,"original-fs":void 0}],7:[function(t,e,r){"use strict";function n(t,e,r){t instanceof RegExp&&(t=i(t,r)),e instanceof RegExp&&(e=i(e,r));var n=s(t,e,r);return n&&{start:n[0],end:n[1],pre:r.slice(0,n[0]),body:r.slice(n[0]+t.length,n[1]),post:r.slice(n[1]+e.length)}}function i(t,e){var r=e.match(t);return r?r[0]:null}function s(t,e,r){var n,i,s,o,a,c=r.indexOf(t),h=r.indexOf(e,c+1),u=c;if(c>=0&&h>0){if(t===e)return[c,h];for(n=[],s=r.length;u>=0&&!a;)u==c?(n.push(u),c=r.indexOf(t,u+1)):1==n.length?a=[n.pop(),h]:((i=n.pop())=0?c:h;n.length&&(a=[s,o])}return a}e.exports=n,n.range=s},{}],8:[function(t,e,r){var n=t("concat-map"),i=t("balanced-match");e.exports=function(t){if(!t)return[];"{}"===t.substr(0,2)&&(t="\\{\\}"+t.substr(2));return g(function(t){return t.split("\\\\").join(s).split("\\{").join(o).split("\\}").join(a).split("\\,").join(c).split("\\.").join(h)}(t),!0).map(f)};var s="\0SLASH"+Math.random()+"\0",o="\0OPEN"+Math.random()+"\0",a="\0CLOSE"+Math.random()+"\0",c="\0COMMA"+Math.random()+"\0",h="\0PERIOD"+Math.random()+"\0";function u(t){return parseInt(t,10)==t?parseInt(t,10):t.charCodeAt(0)}function f(t){return t.split(s).join("\\").split(o).join("{").split(a).join("}").split(c).join(",").split(h).join(".")}function l(t){if(!t)return[""];var e=[],r=i("{","}",t);if(!r)return t.split(",");var n=r.pre,s=r.body,o=r.post,a=n.split(",");a[a.length-1]+="{"+s+"}";var c=l(o);return o.length&&(a[a.length-1]+=c.shift(),a.push.apply(a,c)),e.push.apply(e,a),e}function p(t){return"{"+t+"}"}function d(t){return/^-?0\d/.test(t)}function y(t,e){return t<=e}function m(t,e){return t>=e}function g(t,e){var r=[],s=i("{","}",t);if(!s||/\$$/.test(s.pre))return[t];var o,c=/^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(s.body),h=/^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(s.body),f=c||h,v=s.body.indexOf(",")>=0;if(!f&&!v)return s.post.match(/,.*\}/)?g(t=s.pre+"{"+s.body+a+s.post):[t];if(f)o=s.body.split(/\.\./);else if(1===(o=l(s.body)).length&&1===(o=g(o[0],!1).map(p)).length)return(k=s.post.length?g(s.post,!1):[""]).map((function(t){return s.pre+o[0]+t}));var b,w=s.pre,k=s.post.length?g(s.post,!1):[""];if(f){var S=u(o[0]),_=u(o[1]),E=Math.max(o[0].length,o[1].length),x=3==o.length?Math.abs(u(o[2])):1,O=y;_0){var B=new Array(F+1).join("0");I=j<0?"-"+B+I.slice(1):B+I}}b.push(I)}}else b=n(o,(function(t){return g(t,!1)}));for(var L=0;Lthis.endIndex-this.readIndex)throw this.readIndex=this.endIndex,new Error("Failed to read data with length of "+t);var e=this.payloadOffset+this.readIndex;return this.advance(t),e},t.prototype.advance=function(t){var e=n(t,4);this.endIndex-this.readIndext.length&&(this.headerSize=0),this.headerSize!==n(this.headerSize,4)&&(this.headerSize=0),0===this.headerSize&&(this.header=new Buffer(0))},t.prototype.createIterator=function(){return new i(this)},t.prototype.toBuffer=function(){return this.header.slice(0,this.headerSize+this.getPayloadSize())},t.prototype.writeBool=function(t){return this.writeInt(t?1:0)},t.prototype.writeInt=function(t){return this.writeBytes(t,4,Buffer.prototype.writeInt32LE)},t.prototype.writeUInt32=function(t){return this.writeBytes(t,4,Buffer.prototype.writeUInt32LE)},t.prototype.writeInt64=function(t){return this.writeBytes(t,8,Buffer.prototype.writeInt64LE)},t.prototype.writeUInt64=function(t){return this.writeBytes(t,8,Buffer.prototype.writeUInt64LE)},t.prototype.writeFloat=function(t){return this.writeBytes(t,4,Buffer.prototype.writeFloatLE)},t.prototype.writeDouble=function(t){return this.writeBytes(t,8,Buffer.prototype.writeDoubleLE)},t.prototype.writeString=function(t){var e=Buffer.byteLength(t,"utf8");return!!this.writeInt(e)&&this.writeBytes(t,e)},t.prototype.setPayloadSize=function(t){return this.header.writeUInt32LE(t,0)},t.prototype.getPayloadSize=function(){return this.header.readUInt32LE(0)},t.prototype.writeBytes=function(t,e,r){var i=n(e,4),s=this.writeOffset+i;s>this.capacityAfterHeader&&this.resize(Math.max(2*this.capacityAfterHeader,s)),null!=r?r.call(this.header,t,this.headerSize+this.writeOffset):this.header.write(t,this.headerSize+this.writeOffset,e);var o=this.headerSize+this.writeOffset+e;return this.header.fill(0,o,o+i-e),this.setPayloadSize(s),this.writeOffset=s,!0},t.prototype.resize=function(t){t=n(t,64),this.header=Buffer.concat([this.header,new Buffer(t)]),this.capacityAfterHeader=t},t}();e.exports=s},{}],11:[function(t,e,r){e.exports=function(t,e){for(var r=[],i=0;i=t.length)return e&&(e[p]=t),r(null,t);c.lastIndex=o;var n=c.exec(t);return l=u,u+=n[0],f=l+n[1],o=c.lastIndex,y[f]||e&&e[f]===f?process.nextTick(g):e&&Object.prototype.hasOwnProperty.call(e,f)?w(e[f]):s.lstat(f,v)}function v(t,n){if(t)return r(t);if(!n.isSymbolicLink())return y[f]=!0,e&&(e[f]=f),process.nextTick(g);if(!i){var o=n.dev.toString(32)+":"+n.ino.toString(32);if(d.hasOwnProperty(o))return b(null,d[o],f)}s.stat(f,(function(t){if(t)return r(t);s.readlink(f,(function(t,e){i||(d[o]=e),b(t,e)}))}))}function b(t,i,s){if(t)return r(t);var o=n.resolve(l,i);e&&(e[s]=o),w(o)}function w(e){t=n.resolve(e,t.slice(o)),m()}m()}},{fs:void 0,path:void 0}],14:[function(t,e,r){function n(t,e){return Object.prototype.hasOwnProperty.call(t,e)}r.setopts=function(t,e,r){r||(r={});if(r.matchBase&&-1===e.indexOf("/")){if(r.noglobstar)throw new Error("base matching requires globstar");e="**/"+e}t.silent=!!r.silent,t.pattern=e,t.strict=!1!==r.strict,t.realpath=!!r.realpath,t.realpathCache=r.realpathCache||Object.create(null),t.follow=!!r.follow,t.dot=!!r.dot,t.mark=!!r.mark,t.nodir=!!r.nodir,t.nodir&&(t.mark=!0);t.sync=!!r.sync,t.nounique=!!r.nounique,t.nonull=!!r.nonull,t.nosort=!!r.nosort,t.nocase=!!r.nocase,t.stat=!!r.stat,t.noprocess=!!r.noprocess,t.absolute=!!r.absolute,t.fs=r.fs||i,t.maxLength=r.maxLength||1/0,t.cache=r.cache||Object.create(null),t.statCache=r.statCache||Object.create(null),t.symlinks=r.symlinks||Object.create(null),function(t,e){t.ignore=e.ignore||[],Array.isArray(t.ignore)||(t.ignore=[t.ignore]);t.ignore.length&&(t.ignore=t.ignore.map(u))}(t,r),t.changedCwd=!1;var o=process.cwd();n(r,"cwd")?(t.cwd=s.resolve(r.cwd),t.changedCwd=t.cwd!==o):t.cwd=o;t.root=r.root||s.resolve(t.cwd,"/"),t.root=s.resolve(t.root),"win32"===process.platform&&(t.root=t.root.replace(/\\/g,"/"));t.cwdAbs=a(t.cwd)?t.cwd:f(t,t.cwd),"win32"===process.platform&&(t.cwdAbs=t.cwdAbs.replace(/\\/g,"/"));t.nomount=!!r.nomount,r.nonegate=!0,r.nocomment=!0,r.allowWindowsEscape=!1,t.minimatch=new c(e,r),t.options=t.minimatch.options},r.ownProp=n,r.makeAbs=f,r.finish=function(t){for(var e=t.nounique,r=e?[]:Object.create(null),n=0,i=t.matches.length;n1)return!0;for(var i=0;ithis.maxLength)return e();if(!this.stat&&p(this.cache,r)){var i=this.cache[r];if(Array.isArray(i)&&(i="DIR"),!n||"DIR"===i)return e(null,i);if(n&&"FILE"===i)return e()}var s=this.statCache[r];if(void 0!==s){if(!1===s)return e(null,s);var o=s.isDirectory()?"DIR":"FILE";return n&&"FILE"===o?e():e(null,o,s)}var a=this,c=d("stat\0"+r,(function(n,i){if(i&&i.isSymbolicLink())return a.fs.stat(r,(function(n,s){n?a._stat2(t,r,null,i,e):a._stat2(t,r,n,s,e)}));a._stat2(t,r,n,i,e)}));c&&a.fs.lstat(r,c)},w.prototype._stat2=function(t,e,r,n,i){if(r&&("ENOENT"===r.code||"ENOTDIR"===r.code))return this.statCache[e]=!1,i();var s="/"===t.slice(-1);if(this.statCache[e]=n,"/"===e.slice(-1)&&n&&!n.isDirectory())return i(null,!1,n);var o=!0;return n&&(o=n.isDirectory()?"DIR":"FILE"),this.cache[e]=this.cache[e]||o,s&&"FILE"===o?i():i(null,o,n)}},{"./common.js":14,"./sync.js":16,assert:void 0,events:void 0,"fs.realpath":12,inflight:17,inherits:18,minimatch:20,once:21,path:void 0,"path-is-absolute":22,util:void 0}],16:[function(t,e,r){e.exports=p,p.GlobSync=d;var n=t("fs.realpath"),i=t("minimatch"),s=(i.Minimatch,t("./glob.js").Glob,t("util"),t("path")),o=t("assert"),a=t("path-is-absolute"),c=t("./common.js"),h=c.setopts,u=c.ownProp,f=c.childrenIgnored,l=c.isIgnored;function p(t,e){if("function"==typeof e||3===arguments.length)throw new TypeError("callback provided to sync glob\nSee: https://github.com/isaacs/node-glob/issues/167");return new d(t,e).found}function d(t,e){if(!t)throw new Error("must provide pattern");if("function"==typeof e||3===arguments.length)throw new TypeError("callback provided to sync glob\nSee: https://github.com/isaacs/node-glob/issues/167");if(!(this instanceof d))return new d(t,e);if(h(this,t,e),this.noprocess)return this;var r=this.minimatch.set.length;this.matches=new Array(r);for(var n=0;nthis.maxLength)return!1;if(!this.stat&&u(this.cache,e)){var n=this.cache[e];if(Array.isArray(n)&&(n="DIR"),!r||"DIR"===n)return n;if(r&&"FILE"===n)return!1}var i=this.statCache[e];if(!i){var s;try{s=this.fs.lstatSync(e)}catch(t){if(t&&("ENOENT"===t.code||"ENOTDIR"===t.code))return this.statCache[e]=!1,!1}if(s&&s.isSymbolicLink())try{i=this.fs.statSync(e)}catch(t){i=s}else i=s}this.statCache[e]=i;n=!0;return i&&(n=i.isDirectory()?"DIR":"FILE"),this.cache[e]=this.cache[e]||n,(!r||"FILE"!==n)&&n},d.prototype._mark=function(t){return c.mark(this,t)},d.prototype._makeAbs=function(t){return c.makeAbs(this,t)}},{"./common.js":14,"./glob.js":15,assert:void 0,"fs.realpath":12,minimatch:20,path:void 0,"path-is-absolute":22,util:void 0}],17:[function(t,e,r){var n=t("wrappy"),i=Object.create(null),s=t("once");function o(t){for(var e=t.length,r=[],n=0;nn?(r.splice(0,n),process.nextTick((function(){e.apply(null,s)}))):delete i[t]}}))}(t))}))},{once:21,wrappy:23}],18:[function(t,e,r){try{var n=t("util");if("function"!=typeof n.inherits)throw"";e.exports=n.inherits}catch(r){e.exports=t("./inherits_browser.js")}},{"./inherits_browser.js":19,util:void 0}],19:[function(t,e,r){"function"==typeof Object.create?e.exports=function(t,e){e&&(t.super_=e,t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}))}:e.exports=function(t,e){if(e){t.super_=e;var r=function(){};r.prototype=e.prototype,t.prototype=new r,t.prototype.constructor=t}}},{}],20:[function(t,e,r){e.exports=l,l.Minimatch=p;var n=function(){try{return t("path")}catch(t){}}()||{sep:"/"};l.sep=n.sep;var i=l.GLOBSTAR=p.GLOBSTAR={},s=t("brace-expansion"),o={"!":{open:"(?:(?!(?:",close:"))[^/]*?)"},"?":{open:"(?:",close:")?"},"+":{open:"(?:",close:")+"},"*":{open:"(?:",close:")*"},"@":{open:"(?:",close:")"}},a="[^/]",c="[^/]*?",h="().*{}+?[]^$\\!".split("").reduce((function(t,e){return t[e]=!0,t}),{});var u=/\/+/;function f(t,e){e=e||{};var r={};return Object.keys(t).forEach((function(e){r[e]=t[e]})),Object.keys(e).forEach((function(t){r[t]=e[t]})),r}function l(t,e,r){return y(e),r||(r={}),!(!r.nocomment&&"#"===e.charAt(0))&&new p(e,r).match(t)}function p(t,e){if(!(this instanceof p))return new p(t,e);y(t),e||(e={}),t=t.trim(),e.allowWindowsEscape||"/"===n.sep||(t=t.split(n.sep).join("/")),this.options=e,this.set=[],this.pattern=t,this.regexp=null,this.negate=!1,this.comment=!1,this.empty=!1,this.partial=!!e.partial,this.make()}function d(t,e){return e||(e=this instanceof p?this.options:{}),t=void 0===t?this.pattern:t,y(t),e.nobrace||!/\{(?:(?!\{).)*\}/.test(t)?[t]:s(t)}l.filter=function(t,e){return e=e||{},function(r,n,i){return l(r,t,e)}},l.defaults=function(t){if(!t||"object"!=typeof t||!Object.keys(t).length)return l;var e=l,r=function(r,n,i){return e(r,n,f(t,i))};return(r.Minimatch=function(r,n){return new e.Minimatch(r,f(t,n))}).defaults=function(r){return e.defaults(f(t,r)).Minimatch},r.filter=function(r,n){return e.filter(r,f(t,n))},r.defaults=function(r){return e.defaults(f(t,r))},r.makeRe=function(r,n){return e.makeRe(r,f(t,n))},r.braceExpand=function(r,n){return e.braceExpand(r,f(t,n))},r.match=function(r,n,i){return e.match(r,n,f(t,i))},r},p.defaults=function(t){return l.defaults(t).Minimatch},p.prototype.debug=function(){},p.prototype.make=function(){var t=this.pattern,e=this.options;if(!e.nocomment&&"#"===t.charAt(0))return void(this.comment=!0);if(!t)return void(this.empty=!0);this.parseNegate();var r=this.globSet=this.braceExpand();e.debug&&(this.debug=function(){console.error.apply(console,arguments)});this.debug(this.pattern,r),r=this.globParts=r.map((function(t){return t.split(u)})),this.debug(this.pattern,r),r=r.map((function(t,e,r){return t.map(this.parse,this)}),this),this.debug(this.pattern,r),r=r.filter((function(t){return-1===t.indexOf(!1)})),this.debug(this.pattern,r),this.set=r},p.prototype.parseNegate=function(){var t=this.pattern,e=!1,r=this.options,n=0;if(r.nonegate)return;for(var i=0,s=t.length;i65536)throw new TypeError("pattern is too long")};p.prototype.parse=function(t,e){y(t);var r=this.options;if("**"===t){if(!r.noglobstar)return i;t="*"}if(""===t)return"";var n,s="",u=!!r.nocase,f=!1,l=[],p=[],d=!1,g=-1,v=-1,b="."===t.charAt(0)?"":r.dot?"(?!(?:^|\\/)\\.{1,2}(?:$|\\/))":"(?!\\.)",w=this;function k(){if(n){switch(n){case"*":s+=c,u=!0;break;case"?":s+=a,u=!0;break;default:s+="\\"+n}w.debug("clearStateChar %j %j",n,s),n=!1}}for(var S,_=0,E=t.length;_-1;B--){var L=p[B],N=s.slice(0,L.reStart),P=s.slice(L.reStart,L.reEnd-8),R=s.slice(L.reEnd-8,L.reEnd),z=s.slice(L.reEnd);R+=z;var D=N.split("(").length-1,T=z;for(_=0;_=0&&!(i=t[s]);s--);for(s=0;s>> no match, partial?",t,l,e,p),l!==a))}if("string"==typeof u?(h=f===u,this.debug("string match",u,f,h)):(h=f.match(u),this.debug("pattern match",u,f,h)),!h)return!1}if(s===a&&o===c)return!0;if(s===a)return r;if(o===c)return s===a-1&&""===t[s];throw new Error("wtf?")}},{"brace-expansion":8,path:void 0}],21:[function(t,e,r){var n=t("wrappy");function i(t){var e=function(){return e.called?e.value:(e.called=!0,e.value=t.apply(this,arguments))};return e.called=!1,e}function s(t){var e=function(){if(e.called)throw new Error(e.onceError);return e.called=!0,e.value=t.apply(this,arguments)},r=t.name||"Function wrapped with `once`";return e.onceError=r+" shouldn't be called more than once",e.called=!1,e}e.exports=n(i),e.exports.strict=n(s),i.proto=i((function(){Object.defineProperty(Function.prototype,"once",{value:function(){return i(this)},configurable:!0}),Object.defineProperty(Function.prototype,"onceStrict",{value:function(){return s(this)},configurable:!0})}))},{wrappy:23}],22:[function(t,e,r){"use strict";function n(t){return"/"===t.charAt(0)}function i(t){var e=/^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/.exec(t),r=e[1]||"",n=Boolean(r&&":"!==r.charAt(1));return Boolean(e[2]||n)}e.exports="win32"===process.platform?i:n,e.exports.posix=n,e.exports.win32=i},{}],23:[function(t,e,r){e.exports=function t(e,r){if(e&&r)return t(e)(r);if("function"!=typeof e)throw new TypeError("need wrapper function");return Object.keys(e).forEach((function(t){n[t]=e[t]})),n;function n(){for(var t=new Array(arguments.length),r=0;r 0: 66 | result.append(line) 67 | return result 68 | 69 | 70 | def ExpandConstants(lines, constants): 71 | for key, value in constants.items(): 72 | lines = lines.replace(key, str(value)) 73 | return lines 74 | 75 | 76 | def ExpandMacros(lines, macros): 77 | for name, macro in macros.items(): 78 | start = lines.find(name + '(', 0) 79 | while start != -1: 80 | # Scan over the arguments 81 | assert lines[start + len(name)] == '(' 82 | height = 1 83 | end = start + len(name) + 1 84 | last_match = end 85 | arg_index = 0 86 | mapping = { } 87 | def add_arg(str): 88 | # Remember to expand recursively in the arguments 89 | replacement = ExpandMacros(str.strip(), macros) 90 | mapping[macro.args[arg_index]] = replacement 91 | while end < len(lines) and height > 0: 92 | # We don't count commas at higher nesting levels. 93 | if lines[end] == ',' and height == 1: 94 | add_arg(lines[last_match:end]) 95 | last_match = end + 1 96 | elif lines[end] in ['(', '{', '[']: 97 | height = height + 1 98 | elif lines[end] in [')', '}', ']']: 99 | height = height - 1 100 | end = end + 1 101 | # Remember to add the last match. 102 | add_arg(lines[last_match:end-1]) 103 | result = macro.expand(mapping) 104 | # Replace the occurrence of the macro with the expansion 105 | lines = lines[:start] + result + lines[end:] 106 | start = lines.find(name + '(', start) 107 | return lines 108 | 109 | 110 | class TextMacro: 111 | def __init__(self, args, body): 112 | self.args = args 113 | self.body = body 114 | def expand(self, mapping): 115 | result = self.body 116 | for key, value in mapping.items(): 117 | result = result.replace(key, value) 118 | return result 119 | 120 | class PythonMacro: 121 | def __init__(self, args, fun): 122 | self.args = args 123 | self.fun = fun 124 | def expand(self, mapping): 125 | args = [] 126 | for arg in self.args: 127 | args.append(mapping[arg]) 128 | return str(self.fun(*args)) 129 | 130 | CONST_PATTERN = re.compile('^const\s+([a-zA-Z0-9_]+)\s*=\s*([^;]*);$') 131 | MACRO_PATTERN = re.compile('^macro\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)\s*=\s*([^;]*);$') 132 | PYTHON_MACRO_PATTERN = re.compile('^python\s+macro\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)\s*=\s*([^;]*);$') 133 | 134 | def ReadMacros(lines): 135 | constants = { } 136 | macros = { } 137 | for line in lines: 138 | hash = line.find('#') 139 | if hash != -1: line = line[:hash] 140 | line = line.strip() 141 | if len(line) == 0: continue 142 | const_match = CONST_PATTERN.match(line) 143 | if const_match: 144 | name = const_match.group(1) 145 | value = const_match.group(2).strip() 146 | constants[name] = value 147 | else: 148 | macro_match = MACRO_PATTERN.match(line) 149 | if macro_match: 150 | name = macro_match.group(1) 151 | args = map(string.strip, macro_match.group(2).split(',')) 152 | body = macro_match.group(3).strip() 153 | macros[name] = TextMacro(args, body) 154 | else: 155 | python_match = PYTHON_MACRO_PATTERN.match(line) 156 | if python_match: 157 | name = python_match.group(1) 158 | args = map(string.strip, python_match.group(2).split(',')) 159 | body = python_match.group(3).strip() 160 | fun = eval("lambda " + ",".join(args) + ': ' + body) 161 | macros[name] = PythonMacro(args, fun) 162 | else: 163 | raise Exception("Illegal line: " + line) 164 | return (constants, macros) 165 | 166 | 167 | TEMPLATE = """ 168 | #include "node/src/env-inl.h" 169 | #include "node/src/node.h" 170 | 171 | namespace yode {{ 172 | 173 | {definitions} 174 | 175 | v8::Local MainSource(node::Environment* env) {{ 176 | return bootstrap_value.ToStringChecked(env->isolate()); 177 | }} 178 | 179 | void DefineJavaScript(node::Environment* env, v8::Local target) {{ 180 | {initializers} 181 | }} 182 | 183 | }} // namespace yode 184 | """ 185 | 186 | ONE_BYTE_STRING = """ 187 | static const uint8_t raw_{var}[] = {{ {data} }}; 188 | static struct : public v8::String::ExternalOneByteStringResource {{ 189 | const char* data() const override {{ 190 | return reinterpret_cast(raw_{var}); 191 | }} 192 | size_t length() const override {{ return sizeof(raw_{var}); }} 193 | void Dispose() override {{ /* Default calls `delete this`. */ }} 194 | v8::Local ToStringChecked(v8::Isolate* isolate) {{ 195 | return v8::String::NewExternalOneByte(isolate, this).ToLocalChecked(); 196 | }} 197 | }} {var}; 198 | """ 199 | 200 | TWO_BYTE_STRING = """ 201 | static const uint16_t raw_{var}[] = {{ {data} }}; 202 | static struct : public v8::String::ExternalStringResource {{ 203 | const uint16_t* data() const override {{ return raw_{var}; }} 204 | size_t length() const override {{ return sizeof(raw_{var}); }} 205 | void Dispose() override {{ /* Default calls `delete this`. */ }} 206 | v8::Local ToStringChecked(v8::Isolate* isolate) {{ 207 | return v8::String::NewExternalTwoByte(isolate, this).ToLocalChecked(); 208 | }} 209 | }} {var}; 210 | """ 211 | 212 | INITIALIZER = """\ 213 | CHECK(target->Set(env->context(), 214 | {key}.ToStringChecked(env->isolate()), 215 | {value}.ToStringChecked(env->isolate())).FromJust()); 216 | """ 217 | 218 | 219 | def Render(var, data): 220 | # Treat non-ASCII as UTF-8 and convert it to UTF-16. 221 | if any(ord(c) > 127 for c in data): 222 | template = TWO_BYTE_STRING 223 | data = list(map(ord, data.decode('utf-8').encode('utf-16be'))) 224 | data = [data[i] * 256 + data[i+1] for i in range(0, len(data), 2)] 225 | data = ToCArray(data) 226 | else: 227 | template = ONE_BYTE_STRING 228 | data = ToCString(data) 229 | return template.format(var=var, data=data) 230 | 231 | 232 | def JS2C(source, target): 233 | modules = [] 234 | consts = {} 235 | macros = {} 236 | macro_lines = [] 237 | 238 | for s in source: 239 | if (os.path.split(str(s))[1]).endswith('macros.py'): 240 | macro_lines.extend(ReadLines(str(s))) 241 | else: 242 | modules.append(s) 243 | 244 | # Process input from all *macro.py files 245 | (consts, macros) = ReadMacros(macro_lines) 246 | 247 | # Build source code lines 248 | definitions = [] 249 | initializers = [] 250 | 251 | for name in modules: 252 | lines = ReadFile(str(name)) 253 | lines = ExpandConstants(lines, consts) 254 | lines = ExpandMacros(lines, macros) 255 | 256 | # On Windows, "./foo.bar" in the .gyp file is passed as "foo.bar" 257 | # so don't assume there is always a slash in the file path. 258 | if '/' in name or '\\' in name: 259 | name = '/'.join(re.split('/|\\\\', name)[1:]) 260 | 261 | name = name.split('.', 1)[0] 262 | var = name.replace('-', '_').replace('/', '_') 263 | key = '%s_key' % var 264 | value = '%s_value' % var 265 | 266 | definitions.append(Render(key, name)) 267 | definitions.append(Render(value, lines)) 268 | initializers.append(INITIALIZER.format(key=key, value=value)) 269 | 270 | # Emit result 271 | output = open(str(target[0]), "w") 272 | output.write(TEMPLATE.format(definitions=''.join(definitions), 273 | initializers=''.join(initializers))) 274 | output.close() 275 | 276 | def main(): 277 | natives = sys.argv[1] 278 | source_files = sys.argv[2:] 279 | JS2C(source_files, [natives]) 280 | 281 | if __name__ == "__main__": 282 | main() 283 | -------------------------------------------------------------------------------- /deps/ninja/ninja: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) 2012 Google Inc. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | OS="$(uname -s)" 8 | THIS_DIR="$(dirname "${0}")" 9 | 10 | function print_help() { 11 | cat <<-EOF 12 | No prebuilt ninja binary was found for this system. 13 | Try building your own binary by doing: 14 | cd ~ 15 | git clone https://github.com/martine/ninja.git -b v1.7.2 16 | cd ninja && ./configure.py --bootstrap 17 | Then add ~/ninja/ to your PATH. 18 | EOF 19 | } 20 | 21 | case "$OS" in 22 | Linux) 23 | MACHINE=$(uname -m) 24 | case "$MACHINE" in 25 | i?86|x86_64) 26 | LONG_BIT=$(getconf LONG_BIT) 27 | # We know we are on x86 but we need to use getconf to determine 28 | # bittage of the userspace install (e.g. when running 32-bit userspace 29 | # on x86_64 kernel) 30 | exec "${THIS_DIR}/ninja-linux${LONG_BIT}" "$@";; 31 | *) 32 | echo Unknown architecture \($MACHINE\) -- unable to run ninja. 33 | print_help 34 | exit 1;; 35 | esac 36 | ;; 37 | Darwin) exec "${THIS_DIR}/ninja-mac" "$@";; 38 | CYGWIN*) exec cmd.exe /c $(cygpath -t windows $0).exe "$@";; 39 | MINGW*) cmd.exe //c $0.exe "$@";; 40 | *) echo "Unsupported OS ${OS}" 41 | print_help 42 | exit 1;; 43 | esac 44 | -------------------------------------------------------------------------------- /deps/ninja/ninja-linux32: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yue/yode/ef213a7617e971536b73481670344cf6d8e3ce12/deps/ninja/ninja-linux32 -------------------------------------------------------------------------------- /deps/ninja/ninja-linux64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yue/yode/ef213a7617e971536b73481670344cf6d8e3ce12/deps/ninja/ninja-linux64 -------------------------------------------------------------------------------- /deps/ninja/ninja-mac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yue/yode/ef213a7617e971536b73481670344cf6d8e3ce12/deps/ninja/ninja-mac -------------------------------------------------------------------------------- /deps/ninja/ninja.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yue/yode/ef213a7617e971536b73481670344cf6d8e3ce12/deps/ninja/ninja.exe -------------------------------------------------------------------------------- /deps/node.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | _register_async_wrap 3 | _register_cares_wrap 4 | _register_fs_event_wrap 5 | _register_inspector 6 | _register_js_stream 7 | _register_buffer 8 | _register_config 9 | _register_contextify 10 | _register_crypto 11 | _register_http_parser 12 | _register_fs 13 | _register_module_wrap 14 | _register_os 15 | _register_serdes 16 | _register_url 17 | _register_util 18 | _register_v8 19 | _register_zlib 20 | _register_pipe_wrap 21 | _register_process_wrap 22 | _register_signal_wrap 23 | _register_spawn_sync 24 | _register_stream_wrap 25 | _register_tcp_wrap 26 | _register_tls_wrap 27 | _register_tty_wrap 28 | _register_udp_wrap 29 | _register_uv 30 | -------------------------------------------------------------------------------- /src/asar_archive.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const os = require('os') 4 | const Pickle = require('pickle') 5 | 6 | class AsarArchive { 7 | constructor(asarPath, offset = null) { 8 | const fd = fs.openSync(asarPath, 'r') 9 | try { 10 | if (offset) 11 | this.contentOffset = offset 12 | else 13 | this.readExtendedMeta(fd, fs.statSync(asarPath)) 14 | 15 | // Read size. 16 | let buffer = Buffer.alloc(8) 17 | fs.readSync(fd, buffer, 0, 8, this.contentOffset) 18 | const size = (new Pickle(buffer)).createIterator().readUInt32() 19 | this.contentOffset += 8 20 | 21 | // Read header. 22 | buffer = Buffer.alloc(size) 23 | fs.readSync(fd, buffer, 0, size, this.contentOffset) 24 | const header = (new Pickle(buffer)).createIterator().readString() 25 | this.header = JSON.parse(header) 26 | this.contentOffset += size 27 | } finally { 28 | fs.closeSync(fd) 29 | } 30 | 31 | // Manage temprary files. 32 | this.tmpFiles = {} 33 | } 34 | 35 | readExtendedMeta(fd, stats) { 36 | // Read last 13 bytes, which are | size(8) | version(1) | magic(4) |. 37 | const buffer = Buffer.alloc(8) 38 | fs.readSync(fd, buffer, 0, 4, stats.size - 4) 39 | const magic = buffer.toString('utf8', 0, 4) 40 | if (magic != 'ASAR') 41 | throw new Error('Not an ASAR archive') 42 | fs.readSync(fd, buffer, 0, 1, stats.size - 5) 43 | const version = buffer.readUInt8(0) 44 | if (version != 2) 45 | throw new Error('Unsupported ASAR version') 46 | fs.readSync(fd, buffer, 0, 8, stats.size - 13) 47 | const size = buffer.readDoubleLE(0) 48 | this.contentOffset = stats.size - size 49 | } 50 | 51 | getTmpDir() { 52 | if (this.tmpDir) 53 | return this.tmpDir 54 | this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'asar-')) 55 | process.once('exit', () => { 56 | for (const name in this.tmpFiles) 57 | fs.unlinkSync(this.tmpFiles[name]) 58 | fs.rmdirSync(this.tmpDir) 59 | }) 60 | return this.tmpDir 61 | } 62 | 63 | getNode(filePath) { 64 | let node = this.header 65 | if (filePath === '') 66 | return node 67 | const components = path.normalize(filePath).split(path.sep) 68 | while (components.length > 0) { 69 | const name = components.shift() 70 | if (name.length == 0) 71 | continue 72 | if (node.files && node.files[name]) 73 | node = node.files[name] 74 | else 75 | return null 76 | } 77 | return node 78 | } 79 | 80 | getFileInfo(filePath) { 81 | const node = this.getNode(filePath) 82 | if (!node) 83 | return null 84 | if (node.files) // dir 85 | return { path: filePath, unpacked: node.unpacked } 86 | if (node.link) 87 | return this.getFileInfo(node.link) 88 | const info = { path: filePath, size: node.size } 89 | if (node.unpacked) 90 | info.unpacked = true 91 | else 92 | info.offset = this.contentOffset + parseInt(node.offset) 93 | return info 94 | } 95 | 96 | readFile(info) { 97 | if (info.unpacked) 98 | throw new Error('Should not use readFile for unpacked path') 99 | if (typeof info.size != 'number') 100 | throw new Error('readFile only works on file') 101 | const buffer = Buffer.alloc(info.size) 102 | const fd = fs.openSync(process.execPath, 'r') 103 | try { 104 | fs.readSync(fd, buffer, 0, info.size, info.offset) 105 | } catch (e) { 106 | return null 107 | } finally { 108 | fs.closeSync(fd) 109 | } 110 | return buffer 111 | } 112 | 113 | copyFileOut(info) { 114 | if (typeof info.size != 'number') 115 | throw new Error('copyFileOut only works on file') 116 | if (this.tmpFiles[info.path]) 117 | return this.tmpFiles[info.path] 118 | if (info.unpacked) 119 | return path.resolve(process.execPath, '..', 'res', info.path) 120 | const tmpFile = path.join(this.getTmpDir(), info.path.replace(/[\\\/]/g, '_')) 121 | fs.writeFileSync(tmpFile, this.readFile(info)) 122 | this.tmpFiles[info.path] = tmpFile 123 | return tmpFile 124 | } 125 | 126 | stat(filePath) { 127 | const node = this.getNode(filePath) 128 | if (!node) 129 | return null 130 | return { 131 | isFile: !node.files && !node.link, 132 | isDirectory: !!node.files, 133 | isLink: !!node.link, 134 | } 135 | } 136 | 137 | realpath(info) { 138 | if (info.unpacked) 139 | return path.resolve(process.execPath, '..', 'res', info.path) 140 | return info.path 141 | } 142 | 143 | readdir(filePath) { 144 | const node = this.getNode(filePath) 145 | if (!node || !node.files) 146 | return null 147 | return Object.keys(node.files) 148 | } 149 | } 150 | 151 | module.exports = AsarArchive 152 | -------------------------------------------------------------------------------- /src/asar_monkey_patch.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process') 2 | const path = require('path') 3 | const util = require('util') 4 | 5 | // The root dir of asar archive. 6 | const rootDir = path._makeLong(path.join(process.execPath, 'asar')) 7 | 8 | // Convert asar archive's Stats object to fs's Stats object. 9 | let nextInode = 0 10 | 11 | // Fake values. 12 | const uid = process.getuid != null ? process.getuid() : 0 13 | const gid = process.getgid != null ? process.getgid() : 0 14 | const fakeTime = new Date() 15 | 16 | // Separate asar package's path from full path. 17 | function splitPath(p) { 18 | if (process.noAsar) 19 | return [false] 20 | if (Buffer.isBuffer(p)) 21 | p = p.toString() 22 | if (typeof p !== 'string') 23 | return [false] 24 | 25 | // We inserted a virtual "asar" root directory to avoid treating execPath as 26 | // directory, which will cause problems in Node.js. 27 | p = path.normalize(path._makeLong(p)) 28 | if (p === rootDir) 29 | return [true, ''] 30 | if (!p.startsWith(rootDir + path.sep)) 31 | return [false] 32 | return [true, p.substr(rootDir.length + 1)] 33 | } 34 | 35 | // Generate fake stats. 36 | function generateStats(stats) { 37 | return { 38 | dev: 1, 39 | ino: ++nextInode, 40 | mode: 33188, 41 | nlink: 1, 42 | uid: uid, 43 | gid: gid, 44 | rdev: 0, 45 | atime: stats.atime || fakeTime, 46 | birthtime: stats.birthtime || fakeTime, 47 | mtime: stats.mtime || fakeTime, 48 | ctime: stats.ctime || fakeTime, 49 | size: stats.size, 50 | isFile: function() { return stats.isFile }, 51 | isDirectory: function() { return stats.isDirectory }, 52 | isSymbolicLink: function() { return stats.isLink }, 53 | isBlockDevice: function() { return false }, 54 | isCharacterDevice: function() { return false }, 55 | isFIFO: function() { return false }, 56 | isSocket: function() { return false }, 57 | } 58 | } 59 | 60 | // Create a ENOENT error. 61 | function notFoundError(filePath, callback) { 62 | const error = new Error(`ENOENT, ${filePath} not found`) 63 | error.code = 'ENOENT' 64 | error.errno = -2 65 | if (typeof callback !== 'function') { 66 | throw error 67 | } else { 68 | process.nextTick(function() { 69 | callback(error) 70 | }) 71 | } 72 | } 73 | 74 | // Create a ENOTDIR error. 75 | function notDirError(callback) { 76 | const error = new Error('ENOTDIR, not a directory') 77 | error.code = 'ENOTDIR' 78 | error.errno = -20 79 | if (typeof callback !== 'function') { 80 | throw error 81 | } else { 82 | process.nextTick(function() { 83 | callback(error) 84 | }) 85 | } 86 | } 87 | 88 | // Create a EACCES error. 89 | function accessError(filePath, callback) { 90 | const error = new Error(`EACCES: permission denied, access '${filePath}'`) 91 | error.code = 'EACCES' 92 | error.errno = -13 93 | if (typeof callback !== 'function') { 94 | throw error 95 | } else { 96 | process.nextTick(function() { 97 | callback(error) 98 | }) 99 | } 100 | } 101 | 102 | // Override APIs that rely on passing file path instead of content to C++. 103 | function overrideAPISync(module, name, arg = 0) { 104 | const old = module[name] 105 | module[name] = function() { 106 | const p = arguments[arg] 107 | const [isAsar, filePath] = splitPath(p) 108 | if (!isAsar) 109 | return old.apply(this, arguments) 110 | 111 | const info = process.asarArchive.getFileInfo(filePath) 112 | if (!info) 113 | return notFoundError(filePath) 114 | 115 | const newPath = process.asarArchive.copyFileOut(info) 116 | if (!newPath) 117 | return notFoundError(filePath) 118 | 119 | arguments[arg] = newPath 120 | return old.apply(this, arguments) 121 | } 122 | } 123 | 124 | function overrideAPI(module, name, arg = 0) { 125 | const old = module[name] 126 | module[name] = function() { 127 | const p = arguments[arg] 128 | const [isAsar, filePath] = splitPath(p) 129 | if (!isAsar) 130 | return old.apply(this, arguments) 131 | 132 | const callback = arguments[arguments.length - 1] 133 | if (typeof callback !== 'function') 134 | return overrideAPISync(module, name, arg) 135 | 136 | const info = process.asarArchive.getFileInfo(filePath) 137 | if (!info) 138 | return notFoundError(filePath, callback) 139 | 140 | const newPath = process.asarArchive.copyFileOut(info) 141 | if (!newPath) 142 | return notFoundError(filePath, callback) 143 | 144 | arguments[arg] = newPath 145 | return old.apply(this, arguments) 146 | } 147 | } 148 | 149 | // Override fs APIs. 150 | exports.wrapFsWithAsar = function(fs) { 151 | const {lstatSync} = fs 152 | fs.lstatSync = function(p, options) { 153 | const [isAsar, filePath] = splitPath(p) 154 | if (!isAsar) 155 | return lstatSync(p, options) 156 | const stats = process.asarArchive.stat(filePath) 157 | if (!stats) { 158 | if (options?.throwIfNoEntry) 159 | notFoundError(filePath) 160 | else 161 | return undefined 162 | } 163 | return generateStats(stats) 164 | } 165 | 166 | const {lstat} = fs 167 | fs.lstat = function(p, options, callback) { 168 | if (typeof options == 'function') { 169 | callback = options 170 | options = {} 171 | } 172 | const [isAsar, filePath] = splitPath(p) 173 | if (!isAsar) 174 | return lstat(p, options, callback) 175 | const stats = process.asarArchive.stat(filePath) 176 | if (!stats) 177 | return notFoundError(filePath, callback) 178 | process.nextTick(function() { 179 | callback(null, generateStats(stats)) 180 | }) 181 | } 182 | 183 | fs.promises.lstat = util.promisify(fs.lstat) 184 | 185 | const {statSync} = fs 186 | fs.statSync = function(p, options) { 187 | const [isAsar] = splitPath(p) 188 | if (!isAsar) 189 | return statSync(p, options) 190 | // Do not distinguish links for now. 191 | return fs.lstatSync(p, options) 192 | } 193 | 194 | const {stat} = fs 195 | fs.stat = function(p, options, callback) { 196 | if (typeof options == 'function') { 197 | callback = options 198 | options = {} 199 | } 200 | const [isAsar] = splitPath(p) 201 | if (!isAsar) 202 | return stat(p, options, callback) 203 | // Do not distinguish links for now. 204 | process.nextTick(function() { 205 | fs.lstat(p, options, callback) 206 | }) 207 | } 208 | 209 | fs.promises.stat = util.promisify(fs.lstat) 210 | 211 | const wrapRealpathSync = function(func) { 212 | return function(p, options) { 213 | const [isAsar, filePath] = splitPath(p) 214 | if (!isAsar) 215 | return func.apply(this, arguments) 216 | const info = process.asarArchive.getFileInfo(filePath) 217 | if (!info) 218 | return notFoundError(filePath) 219 | const real = process.asarArchive.realpath(info) 220 | if (info.unpacked) 221 | return real 222 | else 223 | return path.join(func(process.execPath, options), 'asar', real) 224 | } 225 | } 226 | 227 | const {realpathSync} = fs 228 | fs.realpathSync = wrapRealpathSync(realpathSync) 229 | fs.realpathSync.native = wrapRealpathSync(realpathSync.native); 230 | 231 | const wrapRealpath = function(func) { 232 | return function(p, options, callback) { 233 | const [isAsar, filePath] = splitPath(p) 234 | if (!isAsar) 235 | return func.apply(this, arguments) 236 | if (arguments.length < 3) { 237 | callback = options 238 | options = {} 239 | } 240 | const info = process.asarArchive.getFileInfo(filePath) 241 | if (!info) 242 | return notFoundError(filePath, callback) 243 | const real = process.asarArchive.realpath(info) 244 | if (info.unpacked) { 245 | callback(null, real) 246 | } else { 247 | func(process.execPath, options, function(err, p) { 248 | if (err) 249 | return callback(err) 250 | return callback(null, path.join(p, 'asar', real)) 251 | }) 252 | } 253 | } 254 | } 255 | 256 | const {realpath} = fs 257 | fs.realpath = wrapRealpath(realpath) 258 | fs.realpath.native = wrapRealpath(realpath.native) 259 | 260 | fs.promises.realpath = util.promisify(fs.realpath.native) 261 | 262 | const {exists} = fs 263 | fs.exists = function(p, callback) { 264 | const [isAsar, filePath] = splitPath(p) 265 | if (!isAsar) 266 | return exists(p, callback) 267 | process.nextTick(function() { 268 | callback(process.asarArchive.stat(filePath) !== false) 269 | }) 270 | } 271 | 272 | fs.exists[util.promisify.custom] = function(p) { 273 | const [isAsar, filePath] = splitPath(p) 274 | if (!isAsar) 275 | return exists[util.promisify.custom](p) 276 | return Promise.resolve(process.asarArchive.stat(filePath) !== false) 277 | } 278 | 279 | const {existsSync} = fs 280 | fs.existsSync = function(p) { 281 | const [isAsar, filePath] = splitPath(p) 282 | if (!isAsar) 283 | return existsSync(p) 284 | return process.asarArchive.stat(filePath) !== false 285 | } 286 | 287 | const {access} = fs 288 | fs.access = function(p, mode, callback) { 289 | const [isAsar, filePath] = splitPath(p) 290 | if (!isAsar) 291 | return access.apply(this, arguments) 292 | if (typeof mode === 'function') { 293 | callback = mode 294 | mode = fs.constants.F_OK 295 | } 296 | const info = process.asarArchive.getFileInfo(filePath) 297 | if (!info) 298 | return notFoundError(filePath, callback) 299 | if (info.unpacked) { 300 | const realPath = process.asarArchive.copyFileOut(info) 301 | return fs.access(realPath, mode, callback) 302 | } 303 | const stats = process.asarArchive.stat(filePath) 304 | if (!stats) 305 | return notFoundError(filePath, callback) 306 | if (mode & fs.constants.W_OK) 307 | return accessError(filePath, callback) 308 | process.nextTick(function() { 309 | callback() 310 | }) 311 | } 312 | 313 | fs.promises.access = util.promisify(fs.access) 314 | 315 | const {accessSync} = fs 316 | fs.accessSync = function(p, mode) { 317 | const [isAsar, filePath] = splitPath(p) 318 | if (!isAsar) 319 | return accessSync.apply(this, arguments) 320 | if (mode == null) 321 | mode = fs.constants.F_OK 322 | const info = process.asarArchive.getFileInfo(filePath) 323 | if (!info) 324 | notFoundError(filePath) 325 | if (info.unpacked) { 326 | const realPath = process.asarArchive.copyFileOut(info) 327 | return fs.accessSync(realPath, mode) 328 | } 329 | const stats = process.asarArchive.stat(filePath) 330 | if (!stats) 331 | notFoundError(filePath) 332 | if (mode & fs.constants.W_OK) 333 | accessError(filePath) 334 | } 335 | 336 | const {readFile} = fs 337 | fs.readFile = function(p, options, callback) { 338 | const [isAsar, filePath] = splitPath(p) 339 | if (!isAsar) 340 | return readFile.apply(this, arguments) 341 | if (typeof options === 'function') { 342 | callback = options 343 | options = { 344 | encoding: null 345 | } 346 | } else if (typeof options === 'string') { 347 | options = { 348 | encoding: options 349 | } 350 | } else if (options === null || options === undefined) { 351 | options = { 352 | encoding: null 353 | } 354 | } else if (typeof options !== 'object') { 355 | throw new TypeError('Bad arguments') 356 | } 357 | const {encoding} = options 358 | 359 | const info = process.asarArchive.getFileInfo(filePath) 360 | if (!info) 361 | return notFoundError(filePath, callback) 362 | if (info.size === 0) { 363 | return process.nextTick(function() { 364 | callback(null, encoding ? '' : Buffer.alloc(0)) 365 | }) 366 | } 367 | if (info.unpacked) { 368 | const realPath = process.asarArchive.copyFileOut(info) 369 | return fs.readFile(realPath, options, callback) 370 | } 371 | 372 | const buffer = Buffer.alloc(info.size) 373 | fs.open(process.execPath, 'r', function(error, fd) { 374 | if (error) 375 | return callback(error) 376 | fs.read(fd, buffer, 0, info.size, info.offset, function(error) { 377 | fs.close(fd, () => { 378 | callback(error, encoding ? buffer.toString(encoding) : buffer) 379 | }) 380 | }) 381 | }) 382 | } 383 | 384 | const readFilePromise = fs.promises.readFile 385 | fs.promises.readFile = function(p, options) { 386 | const [isAsar, filePath] = splitPath(p) 387 | if (!isAsar) 388 | return readFilePromise.apply(this, arguments) 389 | 390 | return util.promisify(fs.readFile)(p,options) 391 | } 392 | 393 | const {readFileSync} = fs 394 | fs.readFileSync = function(p, options) { 395 | const [isAsar, filePath] = splitPath(p) 396 | if (!isAsar) 397 | return readFileSync.apply(this, arguments) 398 | const info = process.asarArchive.getFileInfo(filePath) 399 | if (!info) 400 | return notFoundError(filePath) 401 | if (info.size === 0) { 402 | if (options) { 403 | return '' 404 | } else { 405 | return Buffer.alloc(0) 406 | } 407 | } 408 | if (info.unpacked) { 409 | const realPath = process.asarArchive.copyFileOut(info) 410 | return fs.readFileSync(realPath, options) 411 | } 412 | if (!options) { 413 | options = { 414 | encoding: null 415 | } 416 | } else if (typeof options === 'string') { 417 | options = { 418 | encoding: options 419 | } 420 | } else if (typeof options !== 'object') { 421 | throw new TypeError('Bad arguments') 422 | } 423 | const {encoding} = options 424 | const buffer = process.asarArchive.readFile(info) 425 | if (!buffer) 426 | return notFoundError(filePath) 427 | if (encoding) 428 | return buffer.toString(encoding) 429 | else 430 | return buffer 431 | } 432 | 433 | const {readdir} = fs 434 | fs.readdir = function(p, options, callback) { 435 | const [isAsar, filePath] = splitPath(p) 436 | if (!isAsar) 437 | return readdir.apply(this, arguments) 438 | if (typeof options == 'function') { 439 | callback = options 440 | options = {} 441 | } else if (typeof options == 'object') { 442 | throw new Error('fs.readdir with options is not supported for ASAR') 443 | } 444 | const files = process.asarArchive.readdir(filePath) 445 | if (!files) 446 | return notFoundError(filePath, callback) 447 | process.nextTick(function() { 448 | callback(null, files) 449 | }) 450 | } 451 | 452 | fs.promises.readdir = util.promisify(fs.readdir) 453 | 454 | const {readdirSync} = fs 455 | fs.readdirSync = function(p, options) { 456 | const [isAsar, filePath] = splitPath(p) 457 | if (!isAsar) 458 | return readdirSync.apply(this, arguments) 459 | if (typeof options == 'object') 460 | throw new Error('fs.readdir with options is not supported for ASAR') 461 | const files = process.asarArchive.readdir(filePath) 462 | if (!files) 463 | notFoundError(filePath) 464 | return files 465 | } 466 | 467 | const {internalBinding} = require('internal/bootstrap/realm') 468 | const modulesBinding = internalBinding('modules') 469 | const {readPackageJSON} = modulesBinding 470 | modulesBinding.readPackageJSON = function(p, isESM, base, specifier) { 471 | const [isAsar, filePath] = splitPath(p) 472 | if (!isAsar) 473 | return readPackageJSON(p, isESM, base, specifier) 474 | const info = process.asarArchive.getFileInfo(filePath) 475 | if (!info || info.size === 0) 476 | return undefined 477 | const realPath = process.asarArchive.copyFileOut(info) 478 | return readPackageJSON(realPath, isESM, base, specifier) 479 | } 480 | 481 | const internalFsBinding = internalBinding('fs') 482 | const {internalModuleStat} = internalFsBinding 483 | internalFsBinding.internalModuleStat = function(b, p) { 484 | const [isAsar, filePath] = splitPath(p) 485 | if (!isAsar) 486 | return internalModuleStat(b, p) 487 | const stats = process.asarArchive.stat(filePath) 488 | if (!stats) 489 | return -34 // -ENOENT 490 | return stats.isDirectory ? 1 : 0 491 | } 492 | 493 | // Calling mkdir for directory inside asar archive should throw ENOTDIR 494 | // error, but on Windows it throws ENOENT. 495 | // This is to work around the recursive looping bug of mkdirp since it is 496 | // widely used. 497 | if (process.platform === 'win32') { 498 | const {mkdir} = fs 499 | fs.mkdir = function(p, options, callback) { 500 | if (typeof options == 'function') { 501 | callback = options 502 | options = {} 503 | } 504 | const [isAsar, filePath] = splitPath(p) 505 | if (isAsar && filePath.length) 506 | return notDirError(callback) 507 | mkdir(p, options, callback) 508 | } 509 | 510 | const {mkdirSync} = fs 511 | fs.mkdirSync = function(p, options) { 512 | const [isAsar, filePath] = splitPath(p) 513 | if (isAsar && filePath.length) 514 | return notDirError() 515 | return mkdirSync(p, options) 516 | } 517 | } 518 | 519 | // Executing a command string containing a path to an asar 520 | // archive confuses `childProcess.execFile`, which is internally 521 | // called by `childProcess.{exec,execSync}`, causing 522 | // Electron to consider the full command as a single path 523 | // to an archive. 524 | ['exec', 'execSync'].forEach(function(functionName) { 525 | const old = childProcess[functionName] 526 | childProcess[functionName] = function() { 527 | const processNoAsarOriginalValue = process.noAsar 528 | process.noAsar = true 529 | try { 530 | return old.apply(this, arguments) 531 | } finally { 532 | process.noAsar = processNoAsarOriginalValue 533 | } 534 | } 535 | }) 536 | 537 | overrideAPI(fs, 'open') 538 | overrideAPI(childProcess, 'execFile') 539 | overrideAPISync(process, 'dlopen', 1) 540 | overrideAPISync(require('module')._extensions, '.node', 1) 541 | overrideAPISync(fs, 'openSync') 542 | overrideAPISync(childProcess, 'execFileSync') 543 | } 544 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | // setImmediate and process.nextTick makes use of uv_check and uv_prepare to 2 | // run the callbacks, however since we only run uv loop on requests, the 3 | // callbacks wouldn't be called until something else activated the uv loop, 4 | // which would delay the callbacks for arbitrary long time. So we should 5 | // initiatively activate the uv loop once setImmediate and process.nextTick is 6 | // called. 7 | function wrapWithActivateUvLoop(func) { 8 | return function() { 9 | process.activateUvLoop() 10 | return func.apply(this, arguments) 11 | } 12 | } 13 | 14 | (function bootstrap(process, internalRequire, exports) { 15 | // The |require| here is actually |nativeModuleRequire|. 16 | const {BuiltinModule, internalBinding, require} = internalRequire('internal/bootstrap/realm') 17 | const {compileFunctionForCJSLoader} = internalBinding('contextify') 18 | 19 | // Make async method work. 20 | const timers = require('timers') 21 | process.nextTick = wrapWithActivateUvLoop(process.nextTick) 22 | this.setImmediate = wrapWithActivateUvLoop(timers.setImmediate) 23 | this.setTimeout = wrapWithActivateUvLoop(timers.setTimeout) 24 | this.setInterval = wrapWithActivateUvLoop(timers.setInterval) 25 | 26 | // Use a virtual "asar" directory as root. 27 | const dirname = require('path').join(process.execPath, 'asar') 28 | 29 | // Implemented to be loaded by nativeModuleRequire. 30 | class YodeModule { 31 | constructor(id, source) { 32 | this.id = id 33 | this.source = source 34 | this.exports = {} 35 | this.loaded = false 36 | this.loading = false 37 | } 38 | 39 | compileForInternalLoader() { 40 | if (this.loaded || this.loading) 41 | return this.exports 42 | const filename = this.id + '.js' 43 | const {function: compiledWrapper} = compileFunctionForCJSLoader(this.source, filename, false, false) 44 | compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname); 45 | return this.exports 46 | } 47 | } 48 | 49 | // Turn our modules into built-in modules. 50 | for (const id in exports) 51 | BuiltinModule.map.set(id, new YodeModule(id, exports[id], require)) 52 | 53 | try { 54 | // Is the executable concatenated with ASAR archive? 55 | const AsarArchive = require('asar_archive') 56 | process.asarArchive = new AsarArchive(process.execPath/* REPLACE_WITH_OFFSET */) 57 | 58 | // Monkey patch built-in modules. 59 | require('asar_monkey_patch').wrapFsWithAsar(require('fs')) 60 | 61 | // Redirect Node to execute from current ASAR archive. 62 | const {executeUserEntryPoint} = internalRequire('internal/modules/run_main') 63 | process.argv.splice(1, 0, dirname) 64 | executeUserEntryPoint(dirname) 65 | } catch (error) { 66 | // Not an ASAR archive, continue to Node's default routine. 67 | if (error.message != 'Not an ASAR archive') 68 | throw error 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Cheng Zhao. All rights reserved. 2 | // Use of this source code is governed by the MIT license. 3 | 4 | #if defined(WIN32) 5 | #include 6 | #endif 7 | 8 | #include 9 | 10 | #include "src/yode.h" 11 | 12 | #if defined(WIN32) 13 | int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { 14 | // Convert argv to UTF8. 15 | int argc = 0; 16 | wchar_t** wargv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 17 | char** argv = new char*[argc]; 18 | for (int i = 0; i < argc; i++) { 19 | // Compute the size of the required buffer 20 | DWORD size = WideCharToMultiByte(CP_UTF8, 21 | 0, 22 | wargv[i], 23 | -1, 24 | NULL, 25 | 0, 26 | NULL, 27 | NULL); 28 | if (size == 0) { 29 | // This should never happen. 30 | fprintf(stderr, "Could not convert arguments to utf8."); 31 | return 1; 32 | } 33 | // Do the actual conversion 34 | argv[i] = new char[size]; 35 | DWORD result = WideCharToMultiByte(CP_UTF8, 36 | 0, 37 | wargv[i], 38 | -1, 39 | argv[i], 40 | size, 41 | NULL, 42 | NULL); 43 | if (result == 0) { 44 | // This should never happen. 45 | fprintf(stderr, "Could not convert arguments to utf8."); 46 | return 1; 47 | } 48 | } 49 | 50 | #else // !defined(WIN32) 51 | int main(int argc, char* argv[]) { 52 | #endif 53 | 54 | return yode::Start(argc, argv); 55 | } 56 | -------------------------------------------------------------------------------- /src/node_integration.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GitHub, Inc. 2 | // Copyright 2017 Cheng Zhao. All rights reserved. 3 | // Use of this source code is governed by the MIT license. 4 | 5 | #include "src/node_integration.h" 6 | 7 | #include 8 | #include 9 | 10 | #include "node/src/env-inl.h" 11 | #include "node/src/node.h" 12 | #include "node/src/node_internals.h" 13 | 14 | namespace yode { 15 | 16 | NodeIntegration::NodeIntegration() 17 | : uv_loop_(uv_default_loop()), 18 | embed_closed_(false) { 19 | // Interrupt embed polling when a handle is started. 20 | uv_loop_configure(uv_loop_, UV_LOOP_INTERRUPT_ON_IO_CHANGE); 21 | } 22 | 23 | NodeIntegration::~NodeIntegration() { 24 | // Quit the embed thread. 25 | embed_closed_ = true; 26 | uv_sem_post(&embed_sem_); 27 | WakeupEmbedThread(); 28 | 29 | // Wait for everything to be done. 30 | uv_thread_join(&embed_thread_); 31 | 32 | // Clear uv. 33 | uv_sem_destroy(&embed_sem_); 34 | uv_close(reinterpret_cast(&wakeup_handle_), nullptr); 35 | uv_close(reinterpret_cast(&next_tick_handle_), nullptr); 36 | } 37 | 38 | void NodeIntegration::Init() { 39 | // Handle used for waking up the uv loop in embed thread. 40 | // Note that we does not unref this handle to keep the active_handles > 0. 41 | uv_async_init(uv_loop_, &wakeup_handle_, nullptr); 42 | 43 | // Handle used for invoking CallNextTick. 44 | uv_async_init(uv_loop_, &next_tick_handle_, &OnCallNextTick); 45 | uv_unref(reinterpret_cast(&next_tick_handle_)); 46 | 47 | // Start worker that will interrupt main loop when having uv events. 48 | uv_sem_init(&embed_sem_, 0); 49 | uv_thread_create(&embed_thread_, EmbedThreadRunner, this); 50 | } 51 | 52 | void NodeIntegration::UvRunOnce() { 53 | // Get current env. 54 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 55 | v8::HandleScope handle_scope(isolate); 56 | node::Environment* env = node::Environment::GetCurrent(isolate); 57 | CHECK(env); 58 | 59 | // Enter node context while dealing with uv events. 60 | v8::Context::Scope context_scope(env->context()); 61 | 62 | // Perform microtask checkpoint after running JavaScript. 63 | v8::MicrotasksScope::PerformCheckpoint(isolate); 64 | 65 | // Deal with uv events. 66 | uv_run(uv_loop_, UV_RUN_NOWAIT); 67 | 68 | // Tell the worker thread to continue polling. 69 | uv_sem_post(&embed_sem_); 70 | } 71 | 72 | void NodeIntegration::CallNextTick() { 73 | uv_async_send(&next_tick_handle_); 74 | } 75 | 76 | void NodeIntegration::ReleaseHandleRef() { 77 | uv_unref(reinterpret_cast(&wakeup_handle_)); 78 | } 79 | 80 | void NodeIntegration::WakeupMainThread() { 81 | PostTask([this] { 82 | this->UvRunOnce(); 83 | }); 84 | } 85 | 86 | void NodeIntegration::WakeupEmbedThread() { 87 | uv_async_send(&wakeup_handle_); 88 | } 89 | 90 | // static 91 | void NodeIntegration::EmbedThreadRunner(void *arg) { 92 | NodeIntegration* self = static_cast(arg); 93 | 94 | while (true) { 95 | // Wait for the main loop to deal with events. 96 | uv_sem_wait(&self->embed_sem_); 97 | if (self->embed_closed_) 98 | break; 99 | 100 | // Wait for something to happen in uv loop. 101 | // Note that the PollEvents() is implemented by derived classes, so when 102 | // this class is being destructed the PollEvents() would not be available 103 | // anymore. Because of it we must make sure we only invoke PollEvents() 104 | // when this class is alive. 105 | self->PollEvents(); 106 | if (self->embed_closed_) 107 | break; 108 | 109 | // Deal with event in main thread. 110 | self->WakeupMainThread(); 111 | } 112 | } 113 | 114 | // static 115 | void NodeIntegration::OnCallNextTick(uv_async_t* handle) { 116 | // Get current env. 117 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 118 | v8::HandleScope handle_scope(isolate); 119 | node::Environment* env = node::Environment::GetCurrent(isolate); 120 | CHECK(env); 121 | 122 | // The CallbackScope can handle everything for us. 123 | v8::Context::Scope context_scope(env->context()); 124 | node::CallbackScope scope(env->isolate(), v8::Object::New(env->isolate()), 125 | {0, 0}); 126 | } 127 | 128 | } // namespace yode 129 | -------------------------------------------------------------------------------- /src/node_integration.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GitHub, Inc. 2 | // Copyright 2017 Cheng Zhao. All rights reserved. 3 | // Use of this source code is governed by the MIT license. 4 | 5 | #ifndef SRC_NODE_INTEGRATION_H_ 6 | #define SRC_NODE_INTEGRATION_H_ 7 | 8 | #include 9 | 10 | #include "node/deps/uv/include/uv.h" 11 | #include "node/src/node.h" 12 | #include "node/src/util.h" 13 | 14 | namespace yode { 15 | 16 | class NodeIntegration { 17 | public: 18 | static NodeIntegration* Create(); 19 | 20 | virtual ~NodeIntegration(); 21 | 22 | // Prepare for message loop integration. 23 | void Init(); 24 | 25 | // Run the libuv loop for once. 26 | void UvRunOnce(); 27 | 28 | // Handle the nextTick callbacks. 29 | void CallNextTick(); 30 | 31 | // Release our ref to uv handle, so active handles count become 0. 32 | void ReleaseHandleRef(); 33 | 34 | protected: 35 | NodeIntegration(); 36 | 37 | // Called to poll events in new thread. 38 | virtual void PollEvents() = 0; 39 | 40 | // Called to post a task to the main thread, must be thread-safe. 41 | virtual void PostTask(const std::function& task) = 0; 42 | 43 | // Make the main thread run libuv loop. 44 | void WakeupMainThread(); 45 | 46 | // Interrupt the PollEvents. 47 | void WakeupEmbedThread(); 48 | 49 | // Main thread's libuv loop. 50 | uv_loop_t* uv_loop_; 51 | 52 | private: 53 | NodeIntegration(const NodeIntegration&) = delete; 54 | NodeIntegration& operator=(const NodeIntegration&) = delete; 55 | 56 | // Thread to poll uv events. 57 | static void EmbedThreadRunner(void* arg); 58 | 59 | // Handle nextTick callbacks. 60 | static void OnCallNextTick(uv_async_t* handle); 61 | 62 | // Whether the libuv loop has ended. 63 | bool embed_closed_; 64 | 65 | // Handle to wake up uv's loop. 66 | uv_async_t wakeup_handle_; 67 | 68 | // Handle to call nextTick callbacks. 69 | uv_async_t next_tick_handle_; 70 | 71 | // Thread for polling events. 72 | uv_thread_t embed_thread_; 73 | 74 | // Semaphore to wait for main loop in the embed thread. 75 | uv_sem_t embed_sem_; 76 | }; 77 | 78 | } // namespace yode 79 | 80 | #endif // SRC_NODE_INTEGRATION_H_ 81 | -------------------------------------------------------------------------------- /src/node_integration_linux.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GitHub, Inc. 2 | // Copyright 2017 Cheng Zhao. All rights reserved. 3 | // Use of this source code is governed by the MIT license. 4 | 5 | #include "src/node_integration_linux.h" 6 | 7 | #include 8 | #include 9 | 10 | namespace yode { 11 | 12 | namespace { 13 | 14 | // A helper to call destructor for a type. 15 | template 16 | void Delete(void* ptr) { 17 | delete static_cast(ptr); 18 | } 19 | 20 | // Called to executed the task. 21 | gboolean OnSource(std::function* func) { 22 | (*func)(); 23 | return G_SOURCE_REMOVE; 24 | } 25 | 26 | } // namespace 27 | 28 | NodeIntegrationLinux::NodeIntegrationLinux() : epoll_(epoll_create(1)) { 29 | // Listen to the backend fd. 30 | int backend_fd = uv_backend_fd(uv_loop_); 31 | struct epoll_event ev = { 0, 0 }; 32 | ev.events = EPOLLIN; 33 | ev.data.fd = backend_fd; 34 | epoll_ctl(epoll_, EPOLL_CTL_ADD, backend_fd, &ev); 35 | } 36 | 37 | NodeIntegrationLinux::~NodeIntegrationLinux() { 38 | } 39 | 40 | void NodeIntegrationLinux::PollEvents() { 41 | int timeout = uv_backend_timeout(uv_loop_); 42 | 43 | // Wait for new libuv events. 44 | int r; 45 | do { 46 | struct epoll_event ev; 47 | r = epoll_wait(epoll_, &ev, 1, timeout); 48 | } while (r == -1 && errno == EINTR); 49 | } 50 | 51 | void NodeIntegrationLinux::PostTask(const std::function& task) { 52 | g_idle_add_full(G_PRIORITY_DEFAULT, 53 | reinterpret_cast(OnSource), 54 | new std::function(task), 55 | Delete>); 56 | } 57 | 58 | // static 59 | NodeIntegration* NodeIntegration::Create() { 60 | return new NodeIntegrationLinux(); 61 | } 62 | 63 | } // namespace yode 64 | -------------------------------------------------------------------------------- /src/node_integration_linux.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GitHub, Inc. 2 | // Copyright 2017 Cheng Zhao. All rights reserved. 3 | // Use of this source code is governed by the MIT license. 4 | 5 | #ifndef SRC_NODE_INTEGRATION_LINUX_H_ 6 | #define SRC_NODE_INTEGRATION_LINUX_H_ 7 | 8 | #include "src/node_integration.h" 9 | 10 | namespace yode { 11 | 12 | class NodeIntegrationLinux : public NodeIntegration { 13 | public: 14 | NodeIntegrationLinux(); 15 | ~NodeIntegrationLinux() override; 16 | 17 | private: 18 | void PollEvents() override; 19 | void PostTask(const std::function& task) override; 20 | 21 | // Epoll to poll for uv's backend fd. 22 | int epoll_; 23 | }; 24 | 25 | } // namespace yode 26 | 27 | #endif // SRC_NODE_INTEGRATION_LINUX_H_ 28 | -------------------------------------------------------------------------------- /src/node_integration_mac.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GitHub, Inc. 2 | // Copyright 2017 Cheng Zhao. All rights reserved. 3 | // Use of this source code is governed by the MIT license. 4 | 5 | #ifndef SRC_NODE_INTEGRATION_MAC_H_ 6 | #define SRC_NODE_INTEGRATION_MAC_H_ 7 | 8 | #include "src/node_integration.h" 9 | 10 | namespace yode { 11 | 12 | class NodeIntegrationMac : public NodeIntegration { 13 | public: 14 | NodeIntegrationMac(); 15 | ~NodeIntegrationMac() override; 16 | 17 | private: 18 | void PollEvents() override; 19 | void PostTask(const std::function& task) override; 20 | }; 21 | 22 | } // namespace yode 23 | 24 | #endif // SRC_NODE_INTEGRATION_MAC_H_ 25 | -------------------------------------------------------------------------------- /src/node_integration_mac.mm: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GitHub, Inc. 2 | // Copyright 2017 Cheng Zhao. All rights reserved. 3 | // Use of this source code is governed by the MIT license. 4 | 5 | #include "src/node_integration_mac.h" 6 | 7 | #import 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace yode { 16 | 17 | NodeIntegrationMac::NodeIntegrationMac() { 18 | } 19 | 20 | NodeIntegrationMac::~NodeIntegrationMac() { 21 | } 22 | 23 | void NodeIntegrationMac::PollEvents() { 24 | struct timeval tv; 25 | int timeout = uv_backend_timeout(uv_loop_); 26 | if (timeout != -1) { 27 | tv.tv_sec = timeout / 1000; 28 | tv.tv_usec = (timeout % 1000) * 1000; 29 | } 30 | 31 | fd_set readset; 32 | int fd = uv_backend_fd(uv_loop_); 33 | FD_ZERO(&readset); 34 | FD_SET(fd, &readset); 35 | 36 | // Wait for new libuv events. 37 | int r; 38 | do { 39 | r = select(fd + 1, &readset, nullptr, nullptr, 40 | timeout == -1 ? nullptr : &tv); 41 | } while (r == -1 && errno == EINTR); 42 | } 43 | 44 | void NodeIntegrationMac::PostTask(const std::function& task) { 45 | __block std::function callback = task; 46 | dispatch_async(dispatch_get_main_queue(), ^{ 47 | callback(); 48 | }); 49 | } 50 | 51 | // static 52 | NodeIntegration* NodeIntegration::Create() { 53 | return new NodeIntegrationMac(); 54 | } 55 | 56 | } // namespace yode 57 | -------------------------------------------------------------------------------- /src/node_integration_win.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GitHub, Inc. 2 | // Copyright 2017 Cheng Zhao. All rights reserved. 3 | // Use of this source code is governed by the MIT license. 4 | 5 | #include "src/node_integration_win.h" 6 | 7 | #include "node/deps/uv/include/uv.h" 8 | 9 | // http://blogs.msdn.com/oldnewthing/archive/2004/10/25/247180.aspx 10 | extern "C" IMAGE_DOS_HEADER __ImageBase; 11 | 12 | // Returns the HMODULE of the dll the macro was expanded in. 13 | // Only use in cc files, not in h files. 14 | #define CURRENT_MODULE() reinterpret_cast(&__ImageBase) 15 | 16 | namespace yode { 17 | 18 | NodeIntegrationWin::NodeIntegrationWin() { 19 | // Create a message only window to capture timer events. 20 | WNDCLASSEXW wcex; 21 | wcex.cbSize = sizeof(WNDCLASSEX); 22 | wcex.style = NULL; 23 | wcex.lpfnWndProc = WndProc; 24 | wcex.cbClsExtra = 0; 25 | wcex.cbWndExtra = 0; 26 | wcex.hInstance = CURRENT_MODULE(); 27 | wcex.hIcon = NULL; 28 | wcex.hCursor = NULL; 29 | wcex.hbrBackground = NULL; 30 | wcex.lpszMenuName = NULL; 31 | wcex.lpszClassName = L"YodeMessageClass"; 32 | wcex.hIconSm = NULL; 33 | ::RegisterClassExW(&wcex); 34 | message_window_ = ::CreateWindowW(L"YodeMessageClass" , L"TimerWindow", 35 | 0, 10, 10, 10, 10, HWND_MESSAGE, 0, 36 | CURRENT_MODULE(), this); 37 | 38 | InitializeCriticalSectionAndSpinCount(&lock_, 0x00000400); 39 | } 40 | 41 | NodeIntegrationWin::~NodeIntegrationWin() { 42 | DestroyWindow(message_window_); 43 | DeleteCriticalSection(&lock_); 44 | } 45 | 46 | void NodeIntegrationWin::PollEvents() { 47 | DWORD bytes, timeout; 48 | ULONG_PTR key; 49 | OVERLAPPED* overlapped; 50 | 51 | // If there are other kinds of events pending, uv_backend_timeout will 52 | // instruct us not to wait. 53 | timeout = uv_backend_timeout(uv_loop_); 54 | 55 | GetQueuedCompletionStatus(uv_loop_->iocp, 56 | &bytes, 57 | &key, 58 | &overlapped, 59 | timeout); 60 | 61 | // Give the event back so libuv can deal with it. 62 | if (overlapped != NULL) 63 | PostQueuedCompletionStatus(uv_loop_->iocp, 64 | bytes, 65 | key, 66 | overlapped); 67 | } 68 | 69 | void NodeIntegrationWin::PostTask(const std::function& task) { 70 | ::EnterCriticalSection(&lock_); 71 | tasks_[++task_id_] = task; 72 | ::LeaveCriticalSection(&lock_); 73 | ::PostMessage(message_window_, WM_USER, task_id_, 0L); 74 | } 75 | 76 | void NodeIntegrationWin::OnTask(int id) { 77 | std::function task; 78 | { 79 | ::EnterCriticalSection(&lock_); 80 | task = tasks_[id]; 81 | tasks_.erase(id); 82 | ::LeaveCriticalSection(&lock_); 83 | } 84 | task(); 85 | } 86 | 87 | // static 88 | LRESULT CALLBACK NodeIntegrationWin::WndProc( 89 | HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { 90 | NodeIntegrationWin* self = reinterpret_cast( 91 | GetWindowLongPtr(hwnd, GWLP_USERDATA)); 92 | 93 | switch (message) { 94 | // Set up the self before handling WM_CREATE. 95 | case WM_CREATE: { 96 | CREATESTRUCT* cs = reinterpret_cast(lparam); 97 | self = reinterpret_cast(cs->lpCreateParams); 98 | ::SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(self)); 99 | break; 100 | } 101 | 102 | // Clear the pointer to stop calling the self once WM_DESTROY is 103 | // received. 104 | case WM_DESTROY: { 105 | ::SetWindowLongPtr(hwnd, GWLP_USERDATA, NULL); 106 | break; 107 | } 108 | 109 | // Handle the timer message. 110 | case WM_USER: { 111 | self->OnTask(static_cast(wparam)); 112 | return 0; 113 | } 114 | } 115 | 116 | return DefWindowProc(hwnd, message, wparam, lparam); 117 | } 118 | 119 | // static 120 | NodeIntegration* NodeIntegration::Create() { 121 | return new NodeIntegrationWin(); 122 | } 123 | 124 | } // namespace yode 125 | -------------------------------------------------------------------------------- /src/node_integration_win.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 GitHub, Inc. 2 | // Copyright 2017 Cheng Zhao. All rights reserved. 3 | // Use of this source code is governed by the MIT license. 4 | 5 | #ifndef SRC_NODE_INTEGRATION_WIN_H_ 6 | #define SRC_NODE_INTEGRATION_WIN_H_ 7 | 8 | #include 9 | #include 10 | 11 | #include "src/node_integration.h" 12 | 13 | namespace yode { 14 | 15 | class NodeIntegrationWin : public NodeIntegration { 16 | public: 17 | NodeIntegrationWin(); 18 | ~NodeIntegrationWin() override; 19 | 20 | private: 21 | void PollEvents() override; 22 | void PostTask(const std::function& task) override; 23 | 24 | void OnTask(int id); 25 | 26 | static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, 27 | LPARAM lparam); 28 | 29 | CRITICAL_SECTION lock_; 30 | 31 | HWND message_window_; 32 | int task_id_ = 0; 33 | std::unordered_map> tasks_; 34 | }; 35 | 36 | } // namespace yode 37 | 38 | #endif // SRC_NODE_INTEGRATION_WIN_H_ 39 | -------------------------------------------------------------------------------- /src/pickle.js: -------------------------------------------------------------------------------- 1 | // sizeof(T). 2 | var SIZE_INT32 = 4 3 | var SIZE_UINT32 = 4 4 | var SIZE_INT64 = 8 5 | var SIZE_UINT64 = 8 6 | var SIZE_FLOAT = 4 7 | var SIZE_DOUBLE = 8 8 | 9 | // The allocation granularity of the payload. 10 | var PAYLOAD_UNIT = 64 11 | 12 | // Largest JS number. 13 | var CAPACITY_READ_ONLY = 9007199254740992 14 | 15 | // Aligns 'i' by rounding it up to the next multiple of 'alignment'. 16 | var alignInt = function (i, alignment) { 17 | return i + (alignment - (i % alignment)) % alignment 18 | } 19 | 20 | // PickleIterator reads data from a Pickle. The Pickle object must remain valid 21 | // while the PickleIterator object is in use. 22 | var PickleIterator = (function () { 23 | function PickleIterator (pickle) { 24 | this.payload = pickle.header 25 | this.payloadOffset = pickle.headerSize 26 | this.readIndex = 0 27 | this.endIndex = pickle.getPayloadSize() 28 | } 29 | 30 | PickleIterator.prototype.readBool = function () { 31 | return this.readInt() !== 0 32 | } 33 | 34 | PickleIterator.prototype.readInt = function () { 35 | return this.readBytes(SIZE_INT32, Buffer.prototype.readInt32LE) 36 | } 37 | 38 | PickleIterator.prototype.readUInt32 = function () { 39 | return this.readBytes(SIZE_UINT32, Buffer.prototype.readUInt32LE) 40 | } 41 | 42 | PickleIterator.prototype.readInt64 = function () { 43 | return this.readBytes(SIZE_INT64, Buffer.prototype.readInt64LE) 44 | } 45 | 46 | PickleIterator.prototype.readUInt64 = function () { 47 | return this.readBytes(SIZE_UINT64, Buffer.prototype.readUInt64LE) 48 | } 49 | 50 | PickleIterator.prototype.readFloat = function () { 51 | return this.readBytes(SIZE_FLOAT, Buffer.prototype.readFloatLE) 52 | } 53 | 54 | PickleIterator.prototype.readDouble = function () { 55 | return this.readBytes(SIZE_DOUBLE, Buffer.prototype.readDoubleLE) 56 | } 57 | 58 | PickleIterator.prototype.readString = function () { 59 | return this.readBytes(this.readInt()).toString() 60 | } 61 | 62 | PickleIterator.prototype.readBytes = function (length, method) { 63 | var readPayloadOffset = this.getReadPayloadOffsetAndAdvance(length) 64 | if (method != null) { 65 | return method.call(this.payload, readPayloadOffset, length) 66 | } else { 67 | return this.payload.subarray(readPayloadOffset, readPayloadOffset + length) 68 | } 69 | } 70 | 71 | PickleIterator.prototype.getReadPayloadOffsetAndAdvance = function (length) { 72 | if (length > this.endIndex - this.readIndex) { 73 | this.readIndex = this.endIndex 74 | throw new Error('Failed to read data with length of ' + length) 75 | } 76 | var readPayloadOffset = this.payloadOffset + this.readIndex 77 | this.advance(length) 78 | return readPayloadOffset 79 | } 80 | 81 | PickleIterator.prototype.advance = function (size) { 82 | var alignedSize = alignInt(size, SIZE_UINT32) 83 | if (this.endIndex - this.readIndex < alignedSize) { 84 | this.readIndex = this.endIndex 85 | } else { 86 | this.readIndex += alignedSize 87 | } 88 | } 89 | 90 | return PickleIterator 91 | })() 92 | 93 | // This class provides facilities for basic binary value packing and unpacking. 94 | // 95 | // The Pickle class supports appending primitive values (ints, strings, etc.) 96 | // to a pickle instance. The Pickle instance grows its internal memory buffer 97 | // dynamically to hold the sequence of primitive values. The internal memory 98 | // buffer is exposed as the "data" of the Pickle. This "data" can be passed 99 | // to a Pickle object to initialize it for reading. 100 | // 101 | // When reading from a Pickle object, it is important for the consumer to know 102 | // what value types to read and in what order to read them as the Pickle does 103 | // not keep track of the type of data written to it. 104 | // 105 | // The Pickle's data has a header which contains the size of the Pickle's 106 | // payload. It can optionally support additional space in the header. That 107 | // space is controlled by the header_size parameter passed to the Pickle 108 | // constructor. 109 | var Pickle = (function () { 110 | function Pickle (buffer) { 111 | if (buffer) { 112 | this.initFromBuffer(buffer) 113 | } else { 114 | this.initEmpty() 115 | } 116 | } 117 | 118 | Pickle.prototype.initEmpty = function () { 119 | this.header = Buffer.alloc(0) 120 | this.headerSize = SIZE_UINT32 121 | this.capacityAfterHeader = 0 122 | this.writeOffset = 0 123 | this.resize(PAYLOAD_UNIT) 124 | this.setPayloadSize(0) 125 | } 126 | 127 | Pickle.prototype.initFromBuffer = function (buffer) { 128 | this.header = buffer 129 | this.headerSize = buffer.length - this.getPayloadSize() 130 | this.capacityAfterHeader = CAPACITY_READ_ONLY 131 | this.writeOffset = 0 132 | if (this.headerSize > buffer.length) { 133 | this.headerSize = 0 134 | } 135 | if (this.headerSize !== alignInt(this.headerSize, SIZE_UINT32)) { 136 | this.headerSize = 0 137 | } 138 | if (this.headerSize === 0) { 139 | this.header = Buffer.alloc(0) 140 | } 141 | } 142 | 143 | Pickle.prototype.createIterator = function () { 144 | return new PickleIterator(this) 145 | } 146 | 147 | Pickle.prototype.toBuffer = function () { 148 | return this.header.subarray(0, this.headerSize + this.getPayloadSize()) 149 | } 150 | 151 | Pickle.prototype.writeBool = function (value) { 152 | return this.writeInt(value ? 1 : 0) 153 | } 154 | 155 | Pickle.prototype.writeInt = function (value) { 156 | return this.writeBytes(value, SIZE_INT32, Buffer.prototype.writeInt32LE) 157 | } 158 | 159 | Pickle.prototype.writeUInt32 = function (value) { 160 | return this.writeBytes(value, SIZE_UINT32, Buffer.prototype.writeUInt32LE) 161 | } 162 | 163 | Pickle.prototype.writeInt64 = function (value) { 164 | return this.writeBytes(value, SIZE_INT64, Buffer.prototype.writeInt64LE) 165 | } 166 | 167 | Pickle.prototype.writeUInt64 = function (value) { 168 | return this.writeBytes(value, SIZE_UINT64, Buffer.prototype.writeUInt64LE) 169 | } 170 | 171 | Pickle.prototype.writeFloat = function (value) { 172 | return this.writeBytes(value, SIZE_FLOAT, Buffer.prototype.writeFloatLE) 173 | } 174 | 175 | Pickle.prototype.writeDouble = function (value) { 176 | return this.writeBytes(value, SIZE_DOUBLE, Buffer.prototype.writeDoubleLE) 177 | } 178 | 179 | Pickle.prototype.writeString = function (value) { 180 | var length = Buffer.byteLength(value, 'utf8') 181 | if (!this.writeInt(length)) { 182 | return false 183 | } 184 | return this.writeBytes(value, length) 185 | } 186 | 187 | Pickle.prototype.setPayloadSize = function (payloadSize) { 188 | return this.header.writeUInt32LE(payloadSize, 0) 189 | } 190 | 191 | Pickle.prototype.getPayloadSize = function () { 192 | return this.header.readUInt32LE(0) 193 | } 194 | 195 | Pickle.prototype.writeBytes = function (data, length, method) { 196 | var dataLength = alignInt(length, SIZE_UINT32) 197 | var newSize = this.writeOffset + dataLength 198 | if (newSize > this.capacityAfterHeader) { 199 | this.resize(Math.max(this.capacityAfterHeader * 2, newSize)) 200 | } 201 | if (method != null) { 202 | method.call(this.header, data, this.headerSize + this.writeOffset) 203 | } else { 204 | this.header.write(data, this.headerSize + this.writeOffset, length) 205 | } 206 | var endOffset = this.headerSize + this.writeOffset + length 207 | this.header.fill(0, endOffset, endOffset + dataLength - length) 208 | this.setPayloadSize(newSize) 209 | this.writeOffset = newSize 210 | return true 211 | } 212 | 213 | Pickle.prototype.resize = function (newCapacity) { 214 | newCapacity = alignInt(newCapacity, PAYLOAD_UNIT) 215 | this.header = Buffer.concat([this.header, Buffer.alloc(newCapacity)]) 216 | this.capacityAfterHeader = newCapacity 217 | } 218 | 219 | return Pickle 220 | })() 221 | 222 | module.exports = Pickle 223 | -------------------------------------------------------------------------------- /src/yode.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Cheng Zhao. All rights reserved. 2 | // Use of this source code is governed by the MIT license. 3 | 4 | #include "src/yode.h" 5 | 6 | #include 7 | #include 8 | 9 | #include "node/src/env-inl.h" 10 | #include "node/src/node_errors.h" 11 | #include "src/node_integration.h" 12 | 13 | using node::errors::TryCatchScope; 14 | 15 | namespace yode { 16 | 17 | // Generated from js files. 18 | v8::Local MainSource(node::Environment* env); 19 | void DefineJavaScript(node::Environment* env, v8::Local target); 20 | 21 | namespace { 22 | 23 | // The global instance of NodeIntegration. 24 | std::unique_ptr g_node_integration; 25 | 26 | // Untility function to create a V8 string. 27 | inline v8::Local ToV8(node::Environment* env, const char* str) { 28 | return v8::String::NewFromUtf8( 29 | env->isolate(), str, v8::NewStringType::kNormal).ToLocalChecked(); 30 | } 31 | 32 | // Force running uv loop. 33 | void ActivateUvLoop(const v8::FunctionCallbackInfo& args) { 34 | if (g_node_integration) 35 | g_node_integration->CallNextTick(); 36 | } 37 | 38 | // Invoke our bootstrap script. 39 | void Bootstrap(node::Environment* env, 40 | v8::Local process, 41 | v8::Local require) { 42 | // Set native methods. 43 | node::SetMethod( 44 | env->context(), env->process_object(), "activateUvLoop", &ActivateUvLoop); 45 | // Set process.versions.yode. 46 | v8::Local versions = env->process_object()->Get( 47 | env->context(), ToV8(env, "versions")).ToLocalChecked(); 48 | versions.As()->Set( 49 | env->context(), ToV8(env, "yode"), ToV8(env, "0.11.1")).ToChecked(); 50 | // Initialize GUI after Node gets initialized. 51 | Init(env); 52 | // Put our scripts into |exports|. 53 | v8::Local exports = v8::Object::New(env->isolate()); 54 | DefineJavaScript(env, exports); 55 | // Get the |bootstrap| function. 56 | v8::ScriptOrigin origin( 57 | env->isolate(), 58 | node::FIXED_ONE_BYTE_STRING(env->isolate(), "bootstrap.js")); 59 | v8::MaybeLocal script = 60 | v8::Script::Compile(env->context(), MainSource(env), &origin); 61 | v8::MaybeLocal result = 62 | script.ToLocalChecked()->Run(env->context()); 63 | v8::Local bootstrap = 64 | v8::Local::Cast(result.ToLocalChecked()); 65 | // Invoke the |bootstrap| with |exports|. 66 | std::vector> args = { process, require, exports }; 67 | TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); 68 | result = bootstrap->Call(env->context(), 69 | env->context()->Global(), 70 | args.size(), 71 | args.data()); 72 | if (try_catch.HasCaught()) { 73 | node::AppendExceptionLine(env, 74 | try_catch.Exception(), 75 | try_catch.Message(), 76 | node::FATAL_ERROR); 77 | } else { 78 | result.ToLocalChecked(); 79 | } 80 | } 81 | 82 | // Like SpinEventLoop but replaces the uv_run with RunLoop. 83 | int SpinGUIEventLoop(node::Environment* env) { 84 | env->set_trace_sync_io(env->options()->trace_sync_io); 85 | 86 | // Run GUI message loop. 87 | RunLoop(env); 88 | // No need to keep uv loop alive. 89 | g_node_integration->ReleaseHandleRef(); 90 | // Enter uv loop to handle unfinished uv tasks. 91 | uv_run(env->event_loop(), UV_RUN_DEFAULT); 92 | 93 | node::EmitProcessBeforeExit(env); 94 | env->set_trace_sync_io(false); 95 | env->ForEachRealm([](auto* realm) { realm->VerifyNoStrongBaseObjects(); }); 96 | auto exit_code = node::EmitProcessExitInternal(env); 97 | return static_cast( 98 | exit_code.FromMaybe(node::ExitCode::kGenericUserError)); 99 | } 100 | 101 | } // namespace 102 | 103 | int Start(int argc, char* argv[]) { 104 | // Always enable GC this app is almost always running on desktop. 105 | v8::V8::SetFlagsFromString("--expose_gc", 11); 106 | 107 | // Set up per-process state. 108 | std::vector args(argv, argv + argc); 109 | auto init = node::InitializeOncePerProcess(args); 110 | 111 | // Initialize V8. 112 | auto* platform = init->platform(); 113 | std::unique_ptr array_buffer_allocator( 114 | node::ArrayBufferAllocator::Create()); 115 | auto isolate_params = std::make_unique(); 116 | isolate_params->array_buffer_allocator = array_buffer_allocator.get(); 117 | v8::Isolate* isolate = node::NewIsolate(isolate_params.get(), 118 | uv_default_loop(), 119 | platform); 120 | std::unique_ptr isolate_data(node::CreateIsolateData( 121 | isolate, 122 | uv_default_loop(), 123 | platform, 124 | array_buffer_allocator.get(), 125 | nullptr)); 126 | isolate_data->max_young_gen_size = 127 | isolate_params->constraints.max_young_generation_size_in_bytes(); 128 | 129 | int exit_code = 0; 130 | { 131 | // Create environment. 132 | v8::Locker locker(isolate); 133 | v8::Isolate::Scope isolate_scope(isolate); 134 | v8::HandleScope handle_scope(isolate); 135 | v8::Local context = node::NewContext(isolate); 136 | v8::Context::Scope context_scope(context); 137 | node::EnvironmentFlags::Flags env_flags = 138 | static_cast( 139 | node::EnvironmentFlags::kDefaultFlags | 140 | node::EnvironmentFlags::kHideConsoleWindows); 141 | node::DeleteFnPtr env( 142 | node::CreateEnvironment(isolate_data.get(), 143 | context, 144 | init->args(), 145 | init->exec_args(), 146 | env_flags)); 147 | 148 | // Check if this process should run GUI event loop. 149 | const char* run_as_node = getenv("YODE_RUN_AS_NODE"); 150 | if (!run_as_node || strcmp(run_as_node, "1")) { 151 | g_node_integration.reset(NodeIntegration::Create()); 152 | g_node_integration->Init(); 153 | } 154 | 155 | // Load node. 156 | { 157 | node::LoadEnvironment(env.get(), 158 | node::StartExecutionCallback{}, 159 | g_node_integration ? &Bootstrap : nullptr); 160 | // Enter event loop. 161 | if (g_node_integration) { 162 | g_node_integration->UvRunOnce(); 163 | exit_code = SpinGUIEventLoop(env.get()); 164 | } else { 165 | exit_code = node::SpinEventLoop(env.get()).FromMaybe(1); 166 | } 167 | } 168 | node::Stop(env.get()); 169 | } 170 | isolate_data.reset(); 171 | platform->UnregisterIsolate(isolate); 172 | isolate->Dispose(); 173 | node::TearDownOncePerProcess(); 174 | return exit_code; 175 | } 176 | 177 | } // namespace yode 178 | -------------------------------------------------------------------------------- /src/yode.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/yode.h: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Cheng Zhao. All rights reserved. 2 | // Use of this source code is governed by the MIT license. 3 | 4 | #ifndef SRC_YODE_H_ 5 | #define SRC_YODE_H_ 6 | 7 | #include 8 | 9 | namespace node { 10 | class Environment; 11 | } 12 | 13 | namespace yode { 14 | 15 | // Initialize Node and enter GUI message loop. 16 | int Start(int argc, char* argv[]); 17 | 18 | // Initialize platform specific code. 19 | void Init(node::Environment* env); 20 | 21 | // Run the GUI message loop for once, implemented by different platforms. 22 | void RunLoop(node::Environment* env); 23 | 24 | } // namespace yode 25 | 26 | #endif // SRC_YODE_H_ 27 | -------------------------------------------------------------------------------- /src/yode.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yue/yode/ef213a7617e971536b73481670344cf6d8e3ce12/src/yode.ico -------------------------------------------------------------------------------- /src/yode.rc: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | #include "winresrc.h" 23 | #include "node/src/node_version.h" 24 | 25 | 26 | // Application icon 27 | 1 ICON yode.ico 28 | 29 | 30 | // Version resource 31 | VS_VERSION_INFO VERSIONINFO 32 | FILEVERSION NODE_MAJOR_VERSION,NODE_MINOR_VERSION,NODE_PATCH_VERSION,0 33 | PRODUCTVERSION NODE_MAJOR_VERSION,NODE_MINOR_VERSION,NODE_PATCH_VERSION,0 34 | FILEFLAGSMASK 0x3fL 35 | #ifdef _DEBUG 36 | FILEFLAGS VS_FF_DEBUG 37 | #else 38 | # ifdef NODE_VERSION_IS_RELEASE 39 | FILEFLAGS 0x0L 40 | # else 41 | FILEFLAGS VS_FF_PRERELEASE 42 | # endif 43 | #endif 44 | 45 | FILEOS VOS_NT_WINDOWS32 46 | FILETYPE VFT_APP 47 | FILESUBTYPE 0x0L 48 | BEGIN 49 | BLOCK "StringFileInfo" 50 | BEGIN 51 | BLOCK "040904b0" 52 | BEGIN 53 | VALUE "CompanyName", "Yue" 54 | VALUE "ProductName", "Yode" 55 | VALUE "FileDescription", "Yode: Node.js fork for GUI programming" 56 | VALUE "FileVersion", NODE_EXE_VERSION 57 | VALUE "ProductVersion", NODE_EXE_VERSION 58 | VALUE "OriginalFilename", "yode.exe" 59 | VALUE "InternalName", "yode" 60 | VALUE "LegalCopyright", "Copyright Cheng Zhao. MIT license." 61 | END 62 | END 63 | BLOCK "VarFileInfo" 64 | BEGIN 65 | VALUE "Translation", 0x409, 1200 66 | END 67 | END 68 | -------------------------------------------------------------------------------- /src/yode_linux.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Cheng Zhao. All rights reserved. 2 | // Use of this source code is governed by the MIT license. 3 | 4 | #include "src/yode.h" 5 | 6 | #include 7 | 8 | #include "node/src/env-inl.h" 9 | 10 | namespace yode { 11 | 12 | void Init(node::Environment* env) { 13 | gtk_init(nullptr, nullptr); 14 | } 15 | 16 | void RunLoop(node::Environment* env) { 17 | gtk_main(); // block until quit 18 | } 19 | 20 | } // namespace yode 21 | -------------------------------------------------------------------------------- /src/yode_mac.mm: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Cheng Zhao. All rights reserved. 2 | // Use of this source code is governed by the MIT license. 3 | 4 | #include "src/yode.h" 5 | 6 | #import 7 | 8 | #include "node/src/env-inl.h" 9 | 10 | namespace yode { 11 | 12 | void Init(node::Environment* env) { 13 | [NSApplication sharedApplication]; 14 | [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; 15 | } 16 | 17 | void RunLoop(node::Environment* env) { 18 | [NSApp run]; // block until quit 19 | } 20 | 21 | } // namespace yode 22 | -------------------------------------------------------------------------------- /src/yode_win.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Cheng Zhao. All rights reserved. 2 | // Use of this source code is governed by the MIT license. 3 | 4 | #include "src/yode.h" 5 | 6 | #include "node/src/env-inl.h" 7 | 8 | namespace yode { 9 | 10 | void Init(node::Environment* env) { 11 | #if _DEBUG 12 | // Show system dialog on crash. 13 | ::SetErrorMode(::GetErrorMode() & ~SEM_NOGPFAULTERRORBOX); 14 | #endif 15 | } 16 | 17 | void RunLoop(node::Environment* env) { 18 | MSG msg; 19 | while (::GetMessage(&msg, NULL, 0, 0)) { 20 | ::TranslateMessage(&msg); 21 | ::DispatchMessage(&msg); 22 | } 23 | } 24 | 25 | } // namespace yode 26 | -------------------------------------------------------------------------------- /test/asar_exit_123/index.js: -------------------------------------------------------------------------------- 1 | process.exit(123) 2 | -------------------------------------------------------------------------------- /test/asar_fs_async/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | fs.readFile(__filename, (err, content) => { 4 | process.stdout.write(String(content)) 5 | process.exit(0) 6 | }) 7 | -------------------------------------------------------------------------------- /test/asar_fs_promise/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs/promises'); 2 | 3 | (async function() { 4 | process.stdout.write((await fs.readFile(__filename)).toString()); 5 | process.exit(0) 6 | })() 7 | -------------------------------------------------------------------------------- /test/asar_fs_realpath_dir/dir/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yue/yode/ef213a7617e971536b73481670344cf6d8e3ce12/test/asar_fs_realpath_dir/dir/file -------------------------------------------------------------------------------- /test/asar_fs_realpath_dir/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs') 2 | const path = require('node:path') 3 | 4 | process.stdout.write(fs.realpathSync(path.join(__dirname, 'dir'))) 5 | process.exit(0) 6 | -------------------------------------------------------------------------------- /test/asar_print_filename/index.js: -------------------------------------------------------------------------------- 1 | console.log(__filename) 2 | process.exit(0) 3 | -------------------------------------------------------------------------------- /test/mac.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs/promises') 2 | 3 | // Implementation taken from https://github.com/vercel/pkg/pull/1164. 4 | // 5 | // The MIT License (MIT) 6 | // 7 | // Copyright (c) 2021 Vercel, Inc. 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in all 17 | // copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | // SOFTWARE. 26 | function parseCStr(buf) { 27 | for (let i = 0; i < buf.length; i += 1) { 28 | if (buf[i] === 0) { 29 | return buf.slice(0, i).toString(); 30 | } 31 | } 32 | } 33 | 34 | function patchCommand(type, buf, file) { 35 | // segment_64 36 | if (type === 0x19) { 37 | const name = parseCStr(buf.slice(0, 16)); 38 | 39 | if (name === '__LINKEDIT') { 40 | const fileoff = buf.readBigUInt64LE(32); 41 | const vmsize_patched = BigInt(file.length) - fileoff; 42 | const filesize_patched = vmsize_patched; 43 | 44 | buf.writeBigUInt64LE(vmsize_patched, 24); 45 | buf.writeBigUInt64LE(filesize_patched, 40); 46 | } 47 | } 48 | 49 | // symtab 50 | if (type === 0x2) { 51 | const stroff = buf.readUInt32LE(8); 52 | const strsize_patched = file.length - stroff; 53 | 54 | buf.writeUInt32LE(strsize_patched, 12); 55 | } 56 | } 57 | 58 | async function extendStringTableSize(target) { 59 | const file = await fs.readFile(target) 60 | 61 | const align = 8; 62 | const hsize = 32; 63 | 64 | const ncmds = file.readUInt32LE(16); 65 | const buf = file.slice(hsize); 66 | 67 | for (let offset = 0, i = 0; i < ncmds; i += 1) { 68 | const type = buf.readUInt32LE(offset); 69 | 70 | offset += 4; 71 | const size = buf.readUInt32LE(offset) - 8; 72 | 73 | offset += 4; 74 | patchCommand(type, buf.slice(offset, offset + size), file); 75 | 76 | offset += size; 77 | if (offset & align) { 78 | offset += align - (offset & align); 79 | } 80 | } 81 | 82 | await fs.writeFile(target, file) 83 | } 84 | 85 | module.exports = {extendStringTableSize} 86 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Run the tests defined in this file. 4 | if (require.main == module) { 5 | const Mocha = require('../deps/mocha') 6 | const mocha = new Mocha 7 | mocha.ui('bdd').reporter('tap') 8 | for (let member in require.cache) // make require('test.js') work 9 | delete require.cache[member] 10 | mocha.addFile('test/main.js') 11 | mocha.run((failures) => process.exit(failures)) 12 | return 13 | } 14 | 15 | const assert = require('assert') 16 | const cp = require('child_process') 17 | const path = require('path') 18 | const fs = require('fs') 19 | const asar = require('../deps/asar') 20 | 21 | describe('property', function() { 22 | describe('process.versions.yode', function() { 23 | it('should be defined', function() { 24 | assert.equal(typeof process.versions.yode, 'string') 25 | }) 26 | }) 27 | 28 | describe('process.bootstrap', function() { 29 | it('should be deleted', function() { 30 | assert.equal(process.bootstrap, undefined) 31 | }) 32 | }) 33 | }) 34 | 35 | describe('node', function() { 36 | it('async tasks should work', function(done) { 37 | process.nextTick(() => { 38 | setTimeout(() => { 39 | process.nextTick(() => { 40 | done() 41 | }) 42 | }, 0) 43 | }) 44 | }) 45 | 46 | it('file stream should work', function(done) { 47 | const stream = fs.createReadStream(process.execPath) 48 | stream.on('data', () => {}) 49 | stream.on('end', () => { 50 | done() 51 | }) 52 | }) 53 | 54 | it('network stream should work', function(done) { 55 | require('https').get('https://google.com', (res) => { 56 | res.on('data', () => {}) 57 | res.on('end', () => { 58 | done() 59 | }) 60 | }) 61 | }) 62 | 63 | it('fetch should work', async () => { 64 | const res = await fetch('https://google.com') 65 | assert.equal(res.status, 200) 66 | }) 67 | 68 | it('fork should work', function(done) { 69 | const p = path.join(require('os').tmpdir(), 'yode-fork.js') 70 | fs.writeFileSync(p, "process.send('ok')") 71 | after(function() { 72 | fs.unlinkSync(p) 73 | }) 74 | 75 | const child = cp.fork(p) 76 | let sent = false 77 | child.on('message', (msg) => { 78 | assert.equal(msg, 'ok') 79 | sent = true 80 | }) 81 | child.on('exit', (code) => { 82 | assert.equal(code, 0) 83 | assert.ok(sent) 84 | done() 85 | }) 86 | }) 87 | 88 | it('process can quit', function(done) { 89 | const p = path.join(require('os').tmpdir(), 'yode-exit.js') 90 | fs.writeFileSync(p, "process.exit(123)") 91 | after(function() { 92 | fs.unlinkSync(p) 93 | }) 94 | 95 | const child = cp.fork(p) 96 | child.on('exit', (code) => { 97 | assert.equal(code, 123) 98 | done() 99 | }) 100 | }) 101 | 102 | it('start with asar', async () => { 103 | const result = await packageAndRun('exit_123') 104 | assert.equal(result.status, 123) 105 | }) 106 | 107 | it('start with asar with output', async () => { 108 | const result = await packageAndRun('print_filename') 109 | assert.equal(result.status, 0) 110 | const p = path.join(__dirname, '..', `print_filename_${path.basename(process.execPath)}`, 'asar', 'index.js') 111 | assert.equal(result.stdout.toString().trim(), p) 112 | }) 113 | 114 | it('start with asar with offset', async function() { 115 | this.timeout(10 * 1000) 116 | const result = await packageAndRun('fs_async', changeOffset) 117 | assert.equal(result.status, 0) 118 | assert.ok(result.stdout.toString().includes('fs.readFile(__filename')) 119 | }) 120 | 121 | it('async fs works on asar', async () => { 122 | const result = await packageAndRun('fs_async') 123 | assert.equal(result.status, 0) 124 | assert.ok(result.stdout.toString().includes('fs.readFile(__filename')) 125 | }) 126 | 127 | it('promise fs works on asar', async () => { 128 | const result = await packageAndRun('fs_promise') 129 | assert.equal(result.status, 0) 130 | assert.ok(result.stdout.toString().includes('fs.readFile(__filename')) 131 | }) 132 | 133 | it('fs.realpathSync works on dir in asar', async () => { 134 | const result = await packageAndRun('fs_realpath_dir') 135 | assert.equal(result.status, 0) 136 | assert.ok(result.stdout.toString().endsWith(path.join('asar', 'dir'))); 137 | }) 138 | 139 | it('Promise can resolve', async () => { 140 | await new Promise(resolve => setTimeout(resolve, 100)) 141 | }) 142 | }) 143 | 144 | async function packageAndRun(filename, modifyBinary = null) { 145 | const a = path.join(__dirname, '..', filename + '.asar') 146 | await asar.createPackage(path.join(__dirname, 'asar_' + filename), a) 147 | const p = path.join(__dirname, '..', `${filename}_${path.basename(process.execPath)}`) 148 | fs.writeFileSync(p, fs.readFileSync(process.execPath)) 149 | if (modifyBinary) { 150 | if (process.platform == 'darwin') 151 | cp.execSync(`codesign --remove-signature ${p}`) 152 | modifyBinary(p) 153 | } 154 | fs.appendFileSync(p, fs.readFileSync(a)) 155 | appendMeta(p, a) 156 | fs.chmodSync(p, 0o755) 157 | if (modifyBinary && process.platform == 'darwin') { 158 | await require('./mac').extendStringTableSize(p) 159 | cp.execSync(`codesign --sign - ${p}`) 160 | } 161 | const result = cp.spawnSync(p) 162 | if (result.status !== null) { 163 | // Will be left for debugging if failed to run. 164 | fs.unlinkSync(a) 165 | fs.unlinkSync(p) 166 | } 167 | return result 168 | } 169 | 170 | function changeOffset(target) { 171 | const mark = '/* REPLACE_WITH_OFFSET */' 172 | const data = fs.readFileSync(target) 173 | const pos = data.indexOf(Buffer.from(mark)) 174 | if (pos <= 0) 175 | throw new Error('Unable to find offset mark') 176 | const stat = fs.statSync(target) 177 | const replace = `, ${stat.size}`.padEnd(mark.length, ' ') 178 | data.write(replace, pos) 179 | fs.writeFileSync(target, data) 180 | } 181 | 182 | // Append ASAR meta information at end of target. 183 | function appendMeta(target, asar) { 184 | const stat = fs.statSync(asar) 185 | const meta = Buffer.alloc(8 + 1 + 4) 186 | const asarSize = stat.size + meta.length 187 | meta.writeDoubleLE(asarSize, 0) 188 | meta.writeUInt8(2, 8) 189 | meta.write('ASAR', 9) 190 | fs.appendFileSync(target, meta) 191 | } 192 | -------------------------------------------------------------------------------- /yode.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'yode', 5 | 'type': 'executable', 6 | 'sources': [ 7 | 'node/src/node_snapshot_stub.cc', 8 | 'src/main.cc', 9 | 'src/node_integration.cc', 10 | 'src/node_integration.h', 11 | 'src/node_integration_linux.cc', 12 | 'src/node_integration_linux.h', 13 | 'src/node_integration_mac.h', 14 | 'src/node_integration_mac.mm', 15 | 'src/node_integration_win.cc', 16 | 'src/node_integration_win.h', 17 | 'src/yode.cc', 18 | 'src/yode.h', 19 | 'src/yode_linux.cc', 20 | 'src/yode_mac.mm', 21 | 'src/yode_win.cc', 22 | '<(SHARED_INTERMEDIATE_DIR)/yode_javascript.cc', 23 | ], 24 | 'include_dirs': [ 25 | '.', 26 | 'node/deps/cares/include', # for ares.h 27 | 'node/deps/openssl/openssl/include', # for openssl/opensslv.h 28 | 'node/deps/simdjson', # for simdjson.h 29 | 'node/deps/uv/include', # for uv.h 30 | 'node/src', # for node things 31 | ], 32 | 'defines': [ 33 | 'NODE_HAVE_I18N_SUPPORT=1', 34 | 'NODE_WANT_INTERNALS=1', 35 | 'NODE_SHARED_MODE', 36 | 'HAVE_OPENSSL=1', 37 | 'HAVE_INSPECTOR=1', 38 | ], 39 | 'dependencies': [ 40 | 'yode_js2c#host', 41 | 'node/node.gyp:libnode', 42 | 'node/tools/v8_gypfiles/v8.gyp:v8', 43 | 'node/tools/v8_gypfiles/v8.gyp:v8_libplatform', 44 | 'node/tools/icu/icu-generic.gyp:icui18n', 45 | 'node/tools/icu/icu-generic.gyp:icuuc', 46 | ], 47 | 'conditions': [ 48 | ['OS=="mac"', { 49 | 'link_settings': { 50 | 'libraries': [ 51 | '$(SDKROOT)/System/Library/Frameworks/AppKit.framework', 52 | ], 53 | }, 54 | 'xcode_settings': { 55 | # Generates symbols and strip the binary. 56 | 'DEBUG_INFORMATION_FORMAT': 'dwarf-with-dsym', 57 | 'DEPLOYMENT_POSTPROCESSING': 'YES', 58 | 'STRIP_INSTALLED_PRODUCT': 'YES', 59 | 'STRIPFLAGS': '-x', 60 | # Force loading all objects of node, otherwise some built-in modules 61 | # won't load. 62 | 'OTHER_LDFLAGS': [ 63 | '-Wl,-force_load,<(PRODUCT_DIR)/libnode.a', 64 | ], 65 | }, 66 | }], 67 | ['OS=="win"', { 68 | 'defines': [ 69 | 'NOMINMAX', 70 | ], 71 | 'sources': [ 72 | 'src/yode.rc', 73 | 'deps/node.def', 74 | ], 75 | 'libraries': [ 76 | 'dbghelp.lib', 77 | 'winmm.lib', 78 | ], 79 | 'msvs_settings': { 80 | 'VCManifestTool': { 81 | # Manifest file. 82 | 'EmbedManifest': 'true', 83 | 'AdditionalManifestFiles': 'src/yode.exe.manifest' 84 | }, 85 | 'VCLinkerTool': { 86 | # Using 5.01 would make Windows turn on compatibility mode for 87 | # certain win32 APIs, which would return wrong results. 88 | 'MinimumRequiredVersion': '5.02', 89 | # A win32 GUI program. 90 | 'SubSystem': '2', 91 | # Defined in node target, required for building x86. 92 | 'ImageHasSafeExceptionHandlers': 'false', 93 | # Disable incremental linking, for smaller program. 94 | 'LinkIncremental': 1, 95 | }, 96 | }, 97 | 'msvs_disabled_warnings': [ 98 | 4003, 99 | 4251, 100 | 4244, 101 | 4996, 102 | ], 103 | }], 104 | ['OS in "linux freebsd"', { 105 | 'libraries': [ 106 | '