diff --git a/packages/daemon/index.html b/packages/daemon/index.html index 589dbcf70b..8997254cb8 100644 --- a/packages/daemon/index.html +++ b/packages/daemon/index.html @@ -7,6 +7,6 @@

Daemon

Daemon is running.

- + \ No newline at end of file diff --git a/packages/daemon/package.json b/packages/daemon/package.json index 0882b6aef1..7e7951cfef 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -57,6 +57,7 @@ "browserfs": "^2.0.0", "buffer": "^6.0.3", "events": "^3.3.0", + "idb-kv-store": "^4.5.0", "make-dir": "^4.0.0", "path-browserify": "^1.0.1", "ses": "^0.18.8", diff --git a/packages/daemon/src/browserfs-fix.js b/packages/daemon/src/browserfs-fix.js new file mode 100644 index 0000000000..3e2c911aca --- /dev/null +++ b/packages/daemon/src/browserfs-fix.js @@ -0,0 +1,117 @@ + + +const cb2promise = + (obj, method) => + (...args) => + new Promise((resolve, reject) => { + obj[method](...args, (err, ...rest) => { + if (err) { + reject(err); + } else { + // @ts-ignore + resolve(...rest); + } + }); + }); + + +// const permissionError = pth => { +// // This replicates the exception of `fs.mkdir` with native the +// // `recusive` option when run on an invalid drive under Windows. +// const error = new Error(`operation not permitted, mkdir '${pth}'`); +// error.code = 'EPERM'; +// error.errno = -4048; +// error.path = pth; +// error.syscall = 'mkdir'; +// return error; +// }; + +// const makeDirectory = async (input, options) => { +// const fs = options.fs.promises; +// const path = options.path; + +// const make = async pth => { +// console.log(`makeDirectory ${pth}`); +// // workaround for browserfs bug? +// if (pth === '/') { +// return pth; +// } +// try { +// await fs.mkdir(pth, options.mode); + +// return pth; +// } catch (error) { +// // workaround for browserfs bug? +// if (error.code === 'EEXIST') { +// // continue normally +// return pth; +// } + +// if (error.code === 'EPERM') { +// throw error; +// } + +// if (error.code === 'ENOENT') { +// if (path.dirname(pth) === pth) { +// throw permissionError(pth); +// } + +// if (error.message.includes('null bytes')) { +// throw error; +// } + +// await make(path.dirname(pth)); + +// return make(pth); +// } + +// // try { +// // const stats = await stat(pth); +// // if (!stats.isDirectory()) { +// // throw new Error('The path is not a directory'); +// // } +// // } catch { +// // throw error; +// // } + +// return pth; +// } +// }; + +// return make(path.resolve(input)); +// }; + + + // shim fs + // await new Promise(cb => configure({ + // fs: 'IndexedDB', + // options: {}, + // }, cb)); + // // const fs = BFSRequire('fs'); + // // const fsPath = BFSRequire('path'); + // // @ts-ignore + // fs.promises = { + // readFile: cb2promise(fs, 'readFile'), + // writeFile: cb2promise(fs, 'writeFile'), + // readdir: cb2promise(fs, 'readdir'), + // mkdir: cb2promise(fs, 'mkdir'), + // rm: cb2promise(fs, 'rmdir'), + // rename: cb2promise(fs, 'rename'), + // }; + // // shim mkdir recursive: true + // const _mkDir = fs.mkdir; + // fs.mkdir = (path, options, cb) => { + // if (typeof options === 'function') { + // cb = options; + // options = undefined; + // } + // if (options && options.recursive) { + // makeDirectory(path, { + // fs, + // path: fsPath, + // mode: options.mode, + // }).then(() => cb(null), cb); + // return; + // } + // _mkDir.call(fs, path, options, cb); + // }; \ No newline at end of file diff --git a/packages/daemon/src/build-web.js b/packages/daemon/src/build-web.js index 085dc0760d..312f116cb5 100644 --- a/packages/daemon/src/build-web.js +++ b/packages/daemon/src/build-web.js @@ -21,6 +21,15 @@ const daemonBundleLocation = new URL( '../dist-daemon-web-bundle.js', import.meta.url, ).toString(); +const daemonInitModuleLocation = new URL( + 'daemon-web-init.js', + import.meta.url, +).toString(); +const daemonInitBundleLocation = new URL( + '../dist-daemon-web-init-bundle.js', + import.meta.url, +).toString(); + const workerModuleLocation = new URL( 'worker-web.js', import.meta.url, @@ -29,6 +38,14 @@ const workerBundleLocation = new URL( '../dist-worker-web-bundle.js', import.meta.url, ).toString(); +const workerInitModuleLocation = new URL( + 'worker-web-init.js', + import.meta.url, +).toString(); +const workerInitBundleLocation = new URL( + '../dist-worker-init-bundle.js', + import.meta.url, +).toString(); const bundleOptions = { // node builtin shims for browser @@ -43,6 +60,7 @@ const bundleOptions = { } async function main() { + // daemon kit hermetic bundle await writeBundle( write, read, @@ -50,6 +68,7 @@ async function main() { daemonModuleLocation, bundleOptions, ) + // worker kit hermetic bundle await writeBundle( write, read, @@ -57,6 +76,22 @@ async function main() { workerModuleLocation, bundleOptions, ) + // self-initing daemon + await writeBundle( + write, + read, + daemonInitBundleLocation, + daemonInitModuleLocation, + bundleOptions, + ) + // self-initing worker + await writeBundle( + write, + read, + workerInitBundleLocation, + workerInitModuleLocation, + bundleOptions, + ) } main() \ No newline at end of file diff --git a/packages/daemon/src/daemon-web-init.js b/packages/daemon/src/daemon-web-init.js new file mode 100644 index 0000000000..c3940f3e8c --- /dev/null +++ b/packages/daemon/src/daemon-web-init.js @@ -0,0 +1,11 @@ +import './daemon-web.js' + +globalThis.startDaemon({ + makeWebWorker () { + console.log('making endo worker in subworker') + const worker = new Worker('./dist-worker-web-init-bundle.js', { + name: 'Endo Worker', + }); + return worker; + } +}); \ No newline at end of file diff --git a/packages/daemon/src/daemon-web.js b/packages/daemon/src/daemon-web.js index c809a11e2a..23c5b1c54a 100644 --- a/packages/daemon/src/daemon-web.js +++ b/packages/daemon/src/daemon-web.js @@ -15,7 +15,8 @@ import { Buffer } from 'buffer'; // import { configure, BFSRequire } from 'browserfs'; // import { configure, fs } from './browserfs.mjs'; import fsPath from 'path'; -import { fs } from './web-fs.js'; +import IdbKvStore from 'idb-kv-store'; +import { makeKeyValueFs } from './web-fs.js'; // import makeDirectory from 'make-dir'; import { makePromiseKit } from '@endo/promise-kit'; @@ -40,20 +41,6 @@ const locator = { const { pid, env, kill } = process; -const cb2promise = - (obj, method) => - (...args) => - new Promise((resolve, reject) => { - obj[method](...args, (err, ...rest) => { - if (err) { - reject(err); - } else { - // @ts-ignore - resolve(...rest); - } - }); - }); - const informParentWhenReady = () => { if (process.send) { process.send({ type: 'ready' }); @@ -113,119 +100,19 @@ const crypto = { }, }; -// const permissionError = pth => { -// // This replicates the exception of `fs.mkdir` with native the -// // `recusive` option when run on an invalid drive under Windows. -// const error = new Error(`operation not permitted, mkdir '${pth}'`); -// error.code = 'EPERM'; -// error.errno = -4048; -// error.path = pth; -// error.syscall = 'mkdir'; -// return error; -// }; - -// const makeDirectory = async (input, options) => { -// const fs = options.fs.promises; -// const path = options.path; - -// const make = async pth => { -// console.log(`makeDirectory ${pth}`); -// // workaround for browserfs bug? -// if (pth === '/') { -// return pth; -// } -// try { -// await fs.mkdir(pth, options.mode); - -// return pth; -// } catch (error) { -// // workaround for browserfs bug? -// if (error.code === 'EEXIST') { -// // continue normally -// return pth; -// } - -// if (error.code === 'EPERM') { -// throw error; -// } - -// if (error.code === 'ENOENT') { -// if (path.dirname(pth) === pth) { -// throw permissionError(pth); -// } - -// if (error.message.includes('null bytes')) { -// throw error; -// } - -// await make(path.dirname(pth)); - -// return make(pth); -// } - -// // try { -// // const stats = await stat(pth); -// // if (!stats.isDirectory()) { -// // throw new Error('The path is not a directory'); -// // } -// // } catch { -// // throw error; -// // } - -// return pth; -// } -// }; - -// return make(path.resolve(input)); -// }; +const makePowers = async ({ makeWebWorker }) => { + if (!makeWebWorker) { + throw new Error('makeWebWorker is required'); + } -const makePowers = async () => { - // shim fs - // await new Promise(cb => configure({ - // fs: 'IndexedDB', - // options: {}, - // }, cb)); - // // const fs = BFSRequire('fs'); - // // const fsPath = BFSRequire('path'); - // // @ts-ignore - // fs.promises = { - // readFile: cb2promise(fs, 'readFile'), - // writeFile: cb2promise(fs, 'writeFile'), - // readdir: cb2promise(fs, 'readdir'), - // mkdir: cb2promise(fs, 'mkdir'), - // rm: cb2promise(fs, 'rmdir'), - // rename: cb2promise(fs, 'rename'), - // }; - // // shim mkdir recursive: true - // const _mkDir = fs.mkdir; - // fs.mkdir = (path, options, cb) => { - // if (typeof options === 'function') { - // cb = options; - // options = undefined; - // } - // if (options && options.recursive) { - // makeDirectory(path, { - // fs, - // path: fsPath, - // mode: options.mode, - // }).then(() => cb(null), cb); - // return; - // } - // _mkDir.call(fs, path, options, cb); - // }; + const idbStore = new IdbKvStore('endo-daemon') + const { fs } = makeKeyValueFs(idbStore) const filePowers = makeFilePowers({ fs, path: fsPath }); // @ts-ignore const cryptoPowers = makeCryptoPowers(crypto); - const makeWebWorker = () => { - const worker = new Worker('./dist-worker-web-bundle.js', { - name: 'Endo Worker', - }); - return worker; - } - const powers = makeDaemonicPowers({ locator, url, @@ -236,19 +123,22 @@ const makePowers = async () => { }); const { persistence: daemonicPersistencePowers } = powers; - // try { + // try { // console.log(await fs.promises.readdir('/')) // } catch (e) { + // console.log(e) // debugger // } // try { // console.log(await fs.promises.mkdir('/')) // } catch (e) { + // console.log(e) // debugger // } // try { // console.log(await fs.promises.readdir('/')) // } catch (e) { + // console.log(e) // debugger // } // try { @@ -267,14 +157,15 @@ const makePowers = async () => { return powers; }; -const main = async () => { +const main = async ({ makeWebWorker }) => { const daemonLabel = `daemon in worker`; console.log(`Endo daemon starting in worker`); cancelled.catch(() => { console.log(`Endo daemon stopping in worker`); }); - const powers = await makePowers(); + const powers = await makePowers({ makeWebWorker }); + console.log(`Endo daemon powers created in worker`) const { endoBootstrap, cancelGracePeriod, assignWebletPort } = await makeDaemon(powers, daemonLabel, cancel, cancelled); @@ -302,4 +193,4 @@ const main = async () => { }; -main() \ No newline at end of file +globalThis.startDaemon = main; \ No newline at end of file diff --git a/packages/daemon/src/web-fs.js b/packages/daemon/src/web-fs.js index c49f96fc7b..c4f67b6ba4 100644 --- a/packages/daemon/src/web-fs.js +++ b/packages/daemon/src/web-fs.js @@ -4,12 +4,6 @@ import pathutil from 'path'; import { Buffer } from 'buffer'; -const mountpoint = 'file'; -let mounted = false; - -const processCwd = '/'; -const processUmask = 0o22; - const constants = { UV_FS_SYMLINK_DIR: 1, UV_FS_SYMLINK_JUNCTION: 2, @@ -73,395 +67,464 @@ const permissionsMask = constants.S_IRWXU | constants.S_IRWXG | constants.S_IRWXO; -function Stats() { - this.mode = 0; - if (!(this instanceof Stats)) - return new Stats; +class Stats { + constructor() { + this.mode = 0; + } + _checkModeProperty(property) { + return ((this.mode & constants.S_IFMT) === property); + } + isDirectory() { + return this._checkModeProperty(constants.S_IFDIR); + } + isFile() { + return this._checkModeProperty(constants.S_IFREG); + } + isBlockDevice() { + return this._checkModeProperty(constants.S_IFBLK); + } + isCharacterDevice() { + return this._checkModeProperty(constants.S_IFCHR); + } + isSymbolicLink() { + return this._checkModeProperty(constants.S_IFLNK); + } + isFIFO() { + return this._checkModeProperty(constants.S_IFIFO); + } + isSocket() { + return this._checkModeProperty(constants.S_IFSOCK); + } } -Stats.prototype._checkModeProperty = function(property) { - return ((this.mode & constants.S_IFMT) === property); -}; - -Stats.prototype.isDirectory = function() { - return this._checkModeProperty(constants.S_IFDIR); -}; - -Stats.prototype.isFile = function() { - return this._checkModeProperty(constants.S_IFREG); -}; - -Stats.prototype.isBlockDevice = function() { - return this._checkModeProperty(constants.S_IFBLK); -}; - -Stats.prototype.isCharacterDevice = function() { - return this._checkModeProperty(constants.S_IFCHR); -}; - -Stats.prototype.isSymbolicLink = function() { - return this._checkModeProperty(constants.S_IFLNK); -}; - -Stats.prototype.isFIFO = function() { - return this._checkModeProperty(constants.S_IFIFO); -}; - -Stats.prototype.isSocket = function() { - return this._checkModeProperty(constants.S_IFSOCK); -}; - function error(code, message) { - message = `${code }, ${ message}`; + message = `${code }, ${message}`; const err = new Error(message); // @ts-ignore err.code = code; throw err; } -function normalizePath(path) { - if (typeof path !== 'string') throw new TypeError('path must be a string'); - if (!path) error('ENOENT', "no such file or directory ''"); - if (!mounted) mount(); - if (path.match(/^\//)) { - return pathutil.normalize(path); +/** + * + * @param {{ + * set: (key: string, value: string) => Promise + * get: (key: string) => Promise + * remove: (key: string) => Promise + * }} store + * @param {*} options + */ +export const makeKeyValueFs = (store, options = {}) => { + const { mountpoint = 'file', processCwd = '/' } = options; + let mounted = false; + + const processUmask = 0o22; + + const getNodeMeta = async (path) => { + const data = await store.get(`${mountpoint}-meta://${path}`); + if (data === undefined) { + error('ENOENT', `no such file or directory '${path}'`); + } + return JSON.parse(data); } - else { - return pathutil.normalize(`${processCwd }/${ path}`); + const existsNodeMeta = async (path) => { + try { + await getNodeMeta(path) + return true + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } + } + const setNodeMeta = async (path, data) => { + await store.set(`${mountpoint}-meta://${path}`, JSON.stringify(data)); + } + const removeNodeMeta = async (path) => { + await store.remove(`${mountpoint}-meta://${path}`); + } + const getNodeData = async (path) => { + const data = await store.get(`${mountpoint}://${path}`); + if (data === undefined) { + error('ENOENT', `no such file or directory '${path}'`); + } + return data; + } + const existsNodeData = async (path) => { + try { + await getNodeData(path) + return true + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } + } + const setNodeData = async (path, data) => { + await store.set(`${mountpoint}://${path}`, data); + } + const removeNodeData = async (path) => { + await store.remove(`${mountpoint}://${path}`); } -} -function modeNum(mode) { - if (typeof mode !== 'number') { - if (typeof mode === 'string') { - mode = parseInt(mode, 8); + async function normalizePath (path) { + if (typeof path !== 'string') throw new TypeError('path must be a string'); + if (!path) error('ENOENT', "no such file or directory ''"); + if (!mounted) await mount(); + if (path.match(/^\//)) { + return pathutil.normalize(path); } else { - error('EPERM', `${mode } is not a valid permission mode`); + return pathutil.normalize(`${processCwd}/${path}`); } } - return mode & permissionsMask; -} -function mount() { - if (localStorage.getItem(`${mountpoint }:///`) === null) { - const mode = 0o777 & ~processUmask; - localStorage.setItem(`${mountpoint }-meta:///`, JSON.stringify({ mode: mode | constants.S_IFDIR })); - localStorage.setItem(`${mountpoint }:///`, ''); + function modeNum(mode) { + if (typeof mode !== 'number') { + if (typeof mode === 'string') { + mode = parseInt(mode, 8); + } + else { + error('EPERM', `${mode } is not a valid permission mode`); + } + } + return mode & permissionsMask; } - mounted = true; -} -const stat = (path) => { - path = normalizePath(path); - let data = localStorage.getItem(`${mountpoint }-meta://${ path}`); - if (data === null) { - error('ENOENT', `no such file or directory '${ path }'`); - } - data = JSON.parse(data); - - const stats = new Stats(); - for (const key in data) { - stats[key] = data[key]; - } - return stats; -} + async function mount() { + if (!await existsNodeData('/')) { + const mode = 0o777 & ~processUmask; + await setNodeMeta('/', { mode: mode | constants.S_IFDIR }); + await setNodeData('/', ''); + } + mounted = true; + } -function addDirectoryListing(path) { - const lastslash = path.lastIndexOf('/'); - const filename = path.slice(lastslash + 1); - const dirname = path.slice(0, lastslash) || '/'; - const ls = readdir(dirname); - let listed = false; - for (let i=0; i { + path = await normalizePath(path); + const entry = await getNodeMeta(path); + + const stats = new Stats(); + for (const key in entry) { + stats[key] = entry[key]; + } + return stats; } -} -function openFile(path, stats, options, write) { - const flag = options.flag; - switch (flag) { + async function addDirectoryListing(path) { + const lastslash = path.lastIndexOf('/'); + const filename = path.slice(lastslash + 1); + const dirname = path.slice(0, lastslash) || '/'; + const ls = await readdir(dirname); + let listed = false; + for (let i=0; i { + path = await normalizePath(path); + return await existsNodeMeta(path); + } + + const readFile = async (path, options) => { + path = await normalizePath(path); + let stats; + try { + stats = await stat(path); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; } - break; + } + if (stats && stats.isDirectory()) { + error('EISDIR', `illegal operation on a directory '${path}'`); + } - // move along - case 'w': - case 'w+': - case 'a': - case 'a+': - break; + options = options || {}; + if (typeof options === 'string') { + options = { encoding: options }; + } + if (typeof options !== 'object') { + throw new TypeError('Bad arguments'); + } + const opts = {}; + for (const k in options) opts[k] = options[k]; + opts.flag = opts.flag || 'r'; + opts.mode = opts.mode === undefined ? 0o666 + : opts.mode; - default: - throw new TypeError('Unknown flag'); + openFile(path, stats, opts); + + const buf = Buffer.from(await getNodeData(path), 'base64'); + if (opts.encoding) return buf.toString(opts.encoding); + return buf; } - - if (write) { - if (flag.match(/^r/) && - flag.match(/[^\+]$/)) { - error('EBADF', 'bad file descriptor'); + + const writeFile = async (path, data, options) => { + path = await normalizePath(path); + let stats; + try { + stats = await stat(path); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + if (stats && stats.isDirectory()) { + error('EISDIR', `illegal operation on a directory '${path}'`); + } + + options = options || {}; + if (typeof options === 'string') { + options = { encoding: options }; } + else if (typeof options !== 'object') { + throw new TypeError('Bad arguments'); + } + const opts = {}; + for (const k in options) opts[k] = options[k]; + opts.flag = opts.flag || 'w'; + opts.mode = opts.mode === undefined ? 0o666 + : opts.mode; + + openFile(path, stats, opts, true); + + if (!Buffer.isBuffer(data)) data = Buffer.from(data, opts.encoding); + if (stats && opts.flag.match(/^a/)) { + const prepend = Buffer.from(await getNodeData(path), 'base64'); + data = Buffer.concat([ prepend, data ]); + } + await setNodeData(path, data.toString('base64')); + + addDirectoryListing(path); } - else if (flag.match(/^[w|a]/) && - flag.match(/[^\+]$/)) { - error('EBADF', 'bad file descriptor'); + const readdir = async (path) => { + path = await normalizePath(path); + const stats = await stat(path); + if (!stats.isDirectory()) { + error('ENOTDIR', `not a directory '${path}'`); } - - if (!stats) { - const mode = options.mode & ~processUmask; - stats = { mode: mode | constants.S_IFREG }; - localStorage.setItem(`${mountpoint }://${ path}`, ''); - localStorage.setItem(`${mountpoint }-meta://${ path}`, JSON.stringify(stats)); + + const ls = await getNodeData(path); + if (ls) return ls.split('\n'); + return []; } -} -const exists = (path) => { - path = normalizePath(path); - return localStorage.getItem(`${mountpoint }://${ path}`) !== null; -} - -const readFile = async (path, options) => { - path = normalizePath(path); - try { - var stats = stat(path); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; + const mkdir = async (path, options = {}) => { + path = await normalizePath(path); + if (!await exists(path)) { + if (options.recursive) { + // ignore errror when recursive + } else { + error('EEXIST', `file already exists '${path}'`); + } + } + + let stats + try { + stats = await stat(pathutil.dirname(path)); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + if (options.recursive) { + await mkdir(pathutil.dirname(path), options); + stats = await stat(pathutil.dirname(path)); + } else { + throw err; + } + } + if (!stats.isDirectory()) { + error('ENOTDIR', `not a directory '${path}'`); } + + let mode = options.mode === undefined ? 0o777 : modeNum(options.mode); + mode &= ~processUmask; + stats = { mode: mode | constants.S_IFDIR }; + + await setNodeMeta(path, stats); + await setNodeData(path, ''); + + addDirectoryListing(path); } - if (stats && stats.isDirectory()) { - error('EISDIR', `illegal operation on a directory '${ path }'`); - } - - options = options || {}; - if (typeof options === 'string') { - options = { encoding: options }; - } - if (typeof options !== 'object') { - throw new TypeError('Bad arguments'); - } - const opts = {}; - for (const k in options) opts[k] = options[k]; - opts.flag = opts.flag || 'r'; - opts.mode = opts.mode === undefined ? 0o666 - : opts.mode; - - openFile(path, stats, opts); - - const buf = Buffer.from(localStorage.getItem(`${mountpoint }://${ path}`), 'base64'); - if (opts.encoding) return buf.toString(opts.encoding); - return buf; -} -const writeFile = async (path, data, options) => { - path = normalizePath(path); - try { - var stats = stat(path); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; + const rm = async (path) => { + path = await normalizePath(path); + const stats = await stat(path); + if (stats.isDirectory()) { + error('EPERM', `operation not permitted '${path}'`); } - } - if (stats && stats.isDirectory()) { - error('EISDIR', `illegal operation on a directory '${ path }'`); - } - - options = options || {}; - if (typeof options === 'string') { - options = { encoding: options }; - } - else if (typeof options !== 'object') { - throw new TypeError('Bad arguments'); - } - const opts = {}; - for (const k in options) opts[k] = options[k]; - opts.flag = opts.flag || 'w'; - opts.mode = opts.mode === undefined ? 0o666 - : opts.mode; - - openFile(path, stats, opts, true); - - if (!Buffer.isBuffer(data)) data = Buffer.from(data, opts.encoding); - if (stats && opts.flag.match(/^a/)) { - const prepend = Buffer.from(localStorage.getItem(`${mountpoint }://${ path}`), 'base64'); - data = Buffer.concat([ prepend, data ]); - } - localStorage.setItem(`${mountpoint }://${ path}`, data.toString('base64')); - - addDirectoryListing(path); -} -const readdir = (path) => { - path = normalizePath(path); - const stats = stat(path); - if (!stats.isDirectory()) { - error('ENOTDIR', `not a directory '${ path }'`); - } - - const ls = localStorage.getItem(`${mountpoint }://${ path}`); - if (ls) return ls.split('\n'); - return []; -} + + await removeNodeData(path); + await removeNodeMeta(path); -const mkdir = (path, options = {}) => { - path = normalizePath(path); - if (localStorage.getItem(`${mountpoint }://${ path}`) !== null) { - if (!options.recursive) { - error('EEXIST', `file already exists '${ path }'`); - } - } - - let stats - try { - stats = stat(pathutil.dirname(path)); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; + // update listing in parent dir + let data; + try { + data = await getNodeData(pathutil.dirname(path)); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } } - if (options.recursive) { - mkdir(pathutil.dirname(path), options); - stats = stat(pathutil.dirname(path)); + if (!data) { + return } - } - if (!stats.isDirectory()) { - error('ENOTDIR', `not a directory '${ path }'`); - } - - let mode = options.mode === undefined ? 0o777 : modeNum(options.mode); - mode &= ~processUmask; - stats = { mode: mode | constants.S_IFDIR }; - localStorage.setItem(`${mountpoint }-meta://${ path}`, JSON.stringify(stats)); - - localStorage.setItem(`${mountpoint }://${ path}`, ''); - - addDirectoryListing(path); -} - -const rm = (path) => { - path = normalizePath(path); - const stats = stat(path); - if (stats.isDirectory()) { - error('EPERM', `operation not permitted '${ path }'`); - } - - localStorage.removeItem(`${mountpoint }://${ path}`); - localStorage.removeItem(`${mountpoint }-meta://${ path}`); - - let ls = localStorage.getItem(`${mountpoint }://${ pathutil.dirname(path)}`); - if (ls) { - ls = ls.split('\n'); - const index = ls.indexOf(pathutil.basename(path)); + const lines = data.split('\n'); + const index = lines.indexOf(pathutil.basename(path)); if (index !== -1) { - ls.splice(index, 1); - localStorage.setItem(`${mountpoint }://${ pathutil.dirname(path)}`, ls.join('\n')); + lines.splice(index, 1); + await setNodeData(pathutil.dirname(path), lines.join('\n')); } } -} -const rename = (oldPath, newPath) => { - oldPath = normalizePath(oldPath); - newPath = normalizePath(newPath); - - const oldStats = stat(oldPath); - if (exists(newPath)) { - const newStats = stat(newPath); - if (oldStats.isDirectory()) { - if (!newStats.isDirectory()) { - error('ENOTDIR', `not a directory '${ newPath }'`); + const rename = async (oldPath, newPath) => { + oldPath = await normalizePath(oldPath); + newPath = await normalizePath(newPath); + + const oldStats = await stat(oldPath); + if (await exists(newPath)) { + const newStats = await stat(newPath); + if (oldStats.isDirectory()) { + if (!newStats.isDirectory()) { + error('ENOTDIR', `not a directory '${newPath}'`); + } } + else if (newStats.isDirectory()) { + error('EISDIR', `illegal operation on a directory '${newPath}'`); + } } - else if (newStats.isDirectory()) { - error('EISDIR', `illegal operation on a directory '${ newPath }'`); - } - } - - const oldData = localStorage.getItem(`${mountpoint }://${ oldPath}`); - localStorage.setItem(`${mountpoint }://${ oldPath}`, oldData); - localStorage.removeItem(`${mountpoint }://${ oldPath}`); - - const oldMeta = localStorage.getItem(`${mountpoint }-meta://${ oldPath}`); - localStorage.setItem(`${mountpoint }-meta://${ oldPath}`, oldMeta); - localStorage.removeItem(`${mountpoint }-meta://${ oldPath}`); -}; + + const oldData = await getNodeData(oldPath); + await setNodeData(newPath, oldData); + await removeNodeData(oldPath); + + const oldMeta = await getNodeMeta(oldPath); + await setNodeMeta(newPath, oldMeta); + await removeNodeMeta(oldPath); + }; -const createReadStream = (path, options) => { - let done = false; - const dataP = readFile(path, options); - const asyncIterator = { - next () { - return dataP.then(data => { - const result = { value: data, done }; + const createReadStream = (path, options) => { + let done = false; + const dataP = readFile(path, options); + const asyncIterator = { + next () { + return dataP.then(data => { + const result = { value: data, done }; + done = true; + return result; + }); + }, + return () { done = true; - return result; - }); - }, - return () { - done = true; - }, - throw (error) { - done = true; - throw error; - }, - [Symbol.asyncIterator]() { - return asyncIterator; - }, + }, + throw (error) { + done = true; + throw error; + }, + [Symbol.asyncIterator]() { + return asyncIterator; + }, + } } -} -const createWriteStream = (path, options) => { - let data = Buffer.alloc(0); - const asyncIterator = { - next (chunk) { - if (chunk) { - data = Buffer.concat([data, chunk]); - } - return Promise.resolve({ value: undefined, done: false }); - }, - return () { - return writeFile(path, data, options); - }, - throw (error) { - throw error; - }, - [Symbol.asyncIterator]() { - return asyncIterator; - }, + const createWriteStream = (path, options) => { + let data = Buffer.alloc(0); + const asyncIterator = { + next (chunk) { + if (chunk) { + data = Buffer.concat([data, chunk]); + } + return Promise.resolve({ value: undefined, done: false }); + }, + return () { + return writeFile(path, data, options); + }, + throw (error) { + throw error; + }, + [Symbol.asyncIterator]() { + return asyncIterator; + }, + } } -} -export const fs = { - createReadStream, - createWriteStream, - promises: { - readFile: async (...args) => readFile(...args), - writeFile: async (...args) => writeFile(...args), - readdir: async (...args) => readdir(...args), - mkdir: async (...args) => mkdir(...args), - rm: async (...args) => rm(...args), - rename: async (...args) => rename(...args), - }, -} + const fs = { + createReadStream, + createWriteStream, + promises: { + readFile, + writeFile, + readdir, + mkdir, + rm, + rename, + }, + } + return { fs } +} \ No newline at end of file diff --git a/packages/daemon/src/worker-web-init.js b/packages/daemon/src/worker-web-init.js new file mode 100644 index 0000000000..fa5ba3b09c --- /dev/null +++ b/packages/daemon/src/worker-web-init.js @@ -0,0 +1,3 @@ +import './worker-web.js' + +globalThis.startWorker(); \ No newline at end of file diff --git a/packages/daemon/src/worker-web.js b/packages/daemon/src/worker-web.js index 241268f068..3092e69356 100644 --- a/packages/daemon/src/worker-web.js +++ b/packages/daemon/src/worker-web.js @@ -8,13 +8,16 @@ import '@endo/eventual-send/shim.js'; import '@endo/promise-kit/shim.js'; import '@endo/lockdown/commit.js'; -import { fs } from './web-fs.js'; +import IdbKvStore from 'idb-kv-store'; +import { makeKeyValueFs } from './web-fs.js'; import url from 'url'; import { makePromiseKit } from '@endo/promise-kit'; import { main as workerMain } from './worker.js'; import { makePowers } from './worker-web-powers.js'; +const idbStore = new IdbKvStore('endo-daemon') +const { fs } = makeKeyValueFs(idbStore) const powers = makePowers({ fs, url }); @@ -51,4 +54,4 @@ const main = async () => { } -main() \ No newline at end of file +globalThis.startWorker = main \ No newline at end of file diff --git a/packages/daemon/src/worker.js b/packages/daemon/src/worker.js index cfa2179e92..5657aa06d2 100644 --- a/packages/daemon/src/worker.js +++ b/packages/daemon/src/worker.js @@ -90,7 +90,7 @@ export const makeWorkerFacet = ({ * @param {Promise} cancelled */ export const main = async (powers, locator, uuid, pid, cancel, cancelled) => { - console.error(`Endo worker started on pid ${pid}`); + console.info(`Endo worker started on pid ${pid}`); cancelled.catch(() => { console.error(`Endo worker exiting on pid ${pid}`); }); diff --git a/yarn.lock b/yarn.lock index 9b0ab1c8d6..5bf5fb2d6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6865,6 +6865,14 @@ icss-replace-symbols@1.1.0, icss-replace-symbols@^1.1.0: resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" integrity sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg== +idb-kv-store@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/idb-kv-store/-/idb-kv-store-4.5.0.tgz#74c70534cb80b10880f905e17f8d4969a26175e2" + integrity sha512-snvtAQRforYUI+C2+45L2LBJy/0/uQUffxv8/uwiS98fSUoXHVrFPClgzWZWxT0drwkLHJRm9inZcYzTR42GLA== + dependencies: + inherits "^2.0.3" + promisize "^1.1.2" + ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -10319,6 +10327,11 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" +promisize@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/promisize/-/promisize-1.1.2.tgz#9b47e2cb2ae497eb1ebadc2c4191d64d15c949d1" + integrity sha512-6/X05CD1iri6YyLy6TW7a23HY0igsrb/qetltYKfJznLfzmspWtN/cY/UR0By3M5i13hBDWfmM2P42ovKl3GAw== + promzard@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee"