feat: created an actual kernel and bootloader

BREAKING CHANGE: `process.kernel.loadLibrary('lib/VirtualFS')` replaced by `process.fs`

re #151
This commit is contained in:
ThinLiquid 2024-01-22 13:16:10 +00:00
parent 316df265c3
commit 04ccdf1442
No known key found for this signature in database
GPG key ID: 17538DC3DF6A7387
15 changed files with 828 additions and 596 deletions

View file

@ -7,6 +7,6 @@
<link rel="shortcut icon" href="./src/assets/flow.png" type="image/png"> <link rel="shortcut icon" href="./src/assets/flow.png" type="image/png">
</head> </head>
<body> <body>
<script src="./src/kernel.ts" type="module"></script> <script src="./src/bootloader.ts" type="module"></script>
</body> </body>
</html> </html>

163
package-lock.json generated
View file

@ -9,6 +9,8 @@
"version": "1.1.0", "version": "1.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-to-html": "^0.7.2",
"chalk": "^5.3.0",
"eruda": "^3.0.1", "eruda": "^3.0.1",
"js-ini": "^1.6.0", "js-ini": "^1.6.0",
"material-symbols": "^0.14.3", "material-symbols": "^0.14.3",
@ -303,6 +305,23 @@
"node": ">=v18" "node": ">=v18"
} }
}, },
"node_modules/@commitlint/load/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"optional": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@commitlint/load/node_modules/cosmiconfig": { "node_modules/@commitlint/load/node_modules/cosmiconfig": {
"version": "8.3.6", "version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
@ -400,6 +419,23 @@
"node": ">=v18" "node": ">=v18"
} }
}, },
"node_modules/@commitlint/types/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"optional": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.18.20", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
@ -1877,6 +1913,20 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/ansi-to-html": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz",
"integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==",
"dependencies": {
"entities": "^2.2.0"
},
"bin": {
"ansi-to-html": "bin/ansi-to-html"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ansicolors": { "node_modules/ansicolors": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz",
@ -2376,16 +2426,11 @@
} }
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": { "engines": {
"node": ">=10" "node": "^12.17.0 || ^14.13 || >=16.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
@ -3239,6 +3284,14 @@
"integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==",
"dev": true "dev": true
}, },
"node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-ci": { "node_modules/env-ci": {
"version": "11.0.0", "version": "11.0.0",
"resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz", "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz",
@ -3906,6 +3959,22 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/eslint/node_modules/eslint-scope": { "node_modules/eslint/node_modules/eslint-scope": {
"version": "7.2.2", "version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@ -5066,6 +5135,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/inquirer/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/inquirer/node_modules/escape-string-regexp": { "node_modules/inquirer/node_modules/escape-string-regexp": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -5937,6 +6022,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/log-symbols/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/log-symbols/node_modules/is-unicode-supported": { "node_modules/log-symbols/node_modules/is-unicode-supported": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@ -6055,18 +6156,6 @@
"marked": ">=1 <12" "marked": ">=1 <12"
} }
}, },
"node_modules/marked-terminal/node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/material-symbols": { "node_modules/material-symbols": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.14.5.tgz", "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.14.5.tgz",
@ -9397,6 +9486,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/ora/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/ora/node_modules/is-unicode-supported": { "node_modules/ora/node_modules/is-unicode-supported": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@ -11936,6 +12041,22 @@
"vite": ">=2.0.0" "vite": ">=2.0.0"
} }
}, },
"node_modules/vite-plugin-compression/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/vite-plugin-dynamic-import": { "node_modules/vite-plugin-dynamic-import": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.5.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.5.0.tgz",

View file

@ -34,6 +34,8 @@
"vite-plugin-node-polyfills": "^0.15.0" "vite-plugin-node-polyfills": "^0.15.0"
}, },
"dependencies": { "dependencies": {
"ansi-to-html": "^0.7.2",
"chalk": "^5.3.0",
"eruda": "^3.0.1", "eruda": "^3.0.1",
"js-ini": "^1.6.0", "js-ini": "^1.6.0",
"material-symbols": "^0.14.3", "material-symbols": "^0.14.3",

125
src/bootloader.ts Normal file
View file

@ -0,0 +1,125 @@
/**
* FlowOS Bootloader
*
*/
import Kernel, { spaces } from './kernel'
import HTML from './HTML'
import logo from './assets/flow.png'
const body = new HTML(document.body)
body.html('<style>* { box-sizing: border-box }</style>')
body.style({
margin: '0',
width: '100vw',
height: '100vh'
})
const boot = new HTML('div').styleJs({
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
background: '#11111b',
padding: '100px',
'font-family': 'monospace',
overflow: 'hidden',
userSelect: 'none'
}).appendTo(body)
boot.appendMany(
new HTML('div')
.styleJs({
display: 'flex',
height: '40px',
alignItems: 'center',
gap: '10px'
})
.appendMany(
new HTML('img').attr({
src: logo,
height: '40px'
}),
new HTML('h1').text('FlowOS').styleJs({
color: 'white'
})
),
new HTML('img').attr({
src: logo
}).styleJs({
position: 'absolute',
right: '-8vw',
top: '-7vw',
opacity: '0.03',
height: '50vw',
'pointer-events': 'none',
zIndex: '0'
})
)
const terminal = new HTML('div').style({
color: '#89b4fa',
padding: '10px 3px',
'word-break': 'break-all',
'white-space': 'pre-wrap',
flex: '1',
'user-select': 'text',
position: 'relative',
zIndex: '2'
}).appendTo(boot)
const progress = new HTML('div').style({
width: '0',
background: '#89b4fa',
transition: 'width 0.5s cubic-bezier(1,0,0,1)',
height: '5px'
})
new HTML('div').style({
height: '5px',
width: '100%',
background: '#181825'
}).appendTo(boot)
.append(progress)
const write = (content: string): void => {
terminal.text(terminal.getText() + content)
}
const writeln = (content = ''): void => {
write(`${content}\n`)
}
const originalConsoleLog = console.log
const originalConsoleError = console.error
const originalConsoleWarn = console.warn
const originalConsoleGroup = console.group
window.console.log = (...args: any) => {
originalConsoleLog(...args)
writeln(args)
}
window.console.warn = (...args: any) => {
originalConsoleWarn(...args)
writeln(args)
}
window.console.error = (...args: any) => {
originalConsoleError(...args)
writeln(args)
}
window.console.group = (...args: any) => {
originalConsoleGroup(...args)
writeln(spaces + String(args))
}
try {
const args = new URLSearchParams(window.location.search)
const kernel = new Kernel()
await kernel.boot(boot, progress, args)
} catch (e) {
writeln()
writeln('An error occured while booting FlowOS.')
writeln('Please report this error to Flow Works.')
writeln()
console.error(e.stack)
}

View file

@ -1,27 +1,43 @@
import './assets/style.less' import pkg from '../package.json'
import { version } from '../package.json' import VirtualFS from './system/VirtualFS'
import { v4 as uuid } from 'uuid' import HTML from './HTML'
import { Executable, KernelConfig, Package, Permission, Process, ProcessInfo, FileSystem } from './types'
import ProcessLib from './structures/ProcessLib' import ProcessLib from './structures/ProcessLib'
import ProcLib from './structures/ProcLib'
import { Executable, Process, Package, ProcessInfo, KernelConfig, Permission } from './types'
import semver from 'semver' import semver from 'semver'
import ProcLib from './structures/ProcLib'
import { v4 as uuid } from 'uuid'
import eruda from 'eruda'
import { parse } from 'js-ini'
declare global { export const spaces = ' '
interface Window {
kernel: Kernel const print = {
} ok: (action: string, text: string) => console.log(`[ OK ] ${action} ${text}`),
failed: (action: string, text: string, error: any) => console.error(`[FAILED] ${action} ${text}`),
none: (action: string, text: string) => console.group(`${action} ${text}`)
} }
const params = new URLSearchParams(window.location.search) const handle = async (type: 'target' | 'service' | 'mount', name: string, Instance: any): Promise<boolean | any> => {
try {
async function enableDebug (): Promise<void> { if (type !== 'target') print.none(type === 'mount' ? 'Mounting' : 'Starting', name)
const { default: eruda } = await import('eruda') const instance = typeof Instance === 'object' ? Instance : new Instance()
eruda.init() const data = await instance.init()
return await Promise.resolve() print.ok(
type === 'service'
? 'Started'
: type === 'mount'
? 'Mounted'
: 'Reached target',
name
)
console.groupEnd()
if (typeof Instance === 'object') return data
else return instance
} catch (e) {
print.failed('Failed', `to start ${name}`, e)
console.error(`${spaces}${e.stack.split('\n').join(`\n${spaces}`) as string}`)
return false
} }
if (params.get('debug') != null) {
enableDebug().catch(e => console.error(e))
} }
export default class Kernel { export default class Kernel {
@ -32,31 +48,81 @@ export default class Kernel {
[key: string]: Package [key: string]: Package
} = {} } = {}
fs: any fs: FileSystem | false
config: KernelConfig config: KernelConfig | false
lastPid: number = 0 lastPid: number = 0
constructor (version: string) { constructor () {
this.codename = 'Mochi' this.codename = 'Mochi'
this.version = version this.version = pkg.version
} }
setFS (fs: any, process: ProcessLib): void { async boot (boot: HTML, progress: HTML, args: URLSearchParams): Promise<void> {
if (process.permission === Permission.SYSTEM) { progress.style({ width: '0%' })
this.fs = fs const bootArgs = args.toString().replace(/=($|&)/g, '=true ')
console.log(`FlowOS - v${pkg.version}, Flow Works (c) ${new Date().getFullYear()}`)
console.log()
console.log(`User Agent : ${navigator.userAgent}`)
console.log(`Boot Args : ${bootArgs === '' ? 'None' : bootArgs}`)
console.log()
console.log('...')
console.log()
if (args.has('debug')) eruda.init()
this.fs = await handle('target', 'Virtual File Systems', VirtualFS)
if (this.fs === false) return
this.config = await handle('target', 'FlowOS Configuration', {
init: async () => {
if (this.fs === false) return
return parse(Buffer.from(await this.fs.readFile('/etc/flow')).toString()) as any
} }
})
if (this.config === false) return
const tmp = await handle('mount', 'Temporary Directory (/tmp)', {
init: async () => {
if (this.fs === false) return false
if (await this.fs.exists('/tmp')) {
await this.fs.rmdir('/tmp')
} }
return await this.fs.mkdir('/tmp')
setConfig (data: any, process: ProcessLib): void {
if (process.permission === Permission.SYSTEM) {
this.config = data
document.dispatchEvent(new CustomEvent('config_update', {
detail: {
config: this.config
} }
})) })
if (tmp === false) return
const sw = await handle('service', 'Service Worker', {
init: async () => {
if (this.config === false) return false
const registrations = await navigator.serviceWorker.getRegistrations()
for (const registration of registrations) {
await registration.unregister()
} }
await navigator.serviceWorker.register(`/uv-sw.js?url=${encodeURIComponent(btoa(this.config.SERVER))}&e=${uuid()}`, {
scope: '/service/'
})
}
})
if (sw === false) return
await handle('service', 'Desktop Environment', {
init: () => {
setTimeout(() => {
import('./assets/style.less')
.then(() => {
boot.style({ display: 'none' })
import('material-symbols')
.then(async () => {
if (this.fs === false) return
console.log()
console.log('Welcome to FlowOS!')
console.log()
setTimeout(() => {
this.startExecutable('Desktop', Permission.SYSTEM).catch(e => console.error(e))
}, 750)
})
.catch(e => { throw e })
})
.catch(e => { throw e })
}, 1000)
}
})
} }
async startExecutable (url: string, permission = Permission.USER, data = {}): Promise<{ procLib: ProcessLib, executable: Process } | any> { async startExecutable (url: string, permission = Permission.USER, data = {}): Promise<{ procLib: ProcessLib, executable: Process } | any> {
@ -67,7 +133,7 @@ export default class Kernel {
const importedExecutable = (await module()) as any const importedExecutable = (await module()) as any
executable = importedExecutable.default executable = importedExecutable.default
} catch { } catch {
if (this.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.') if (this.fs === false) throw new Error('Filesystem hasn\'t been initiated.')
const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}` const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
const importedExecutable = await import(dataURL) const importedExecutable = await import(dataURL)
executable = importedExecutable.default executable = importedExecutable.default
@ -111,7 +177,7 @@ export default class Kernel {
const importedExecutable = (await module()) as any const importedExecutable = (await module()) as any
executable = importedExecutable.default executable = importedExecutable.default
} catch { } catch {
if (this.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.') if (this.fs === false) throw new Error('Filesystem hasn\'t been initiated.')
const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}` const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
const importedExecutable = await import(dataURL) const importedExecutable = await import(dataURL)
executable = importedExecutable.default executable = importedExecutable.default
@ -126,9 +192,3 @@ export default class Kernel {
return executable return executable
} }
} }
document.addEventListener('DOMContentLoaded', () => {
import('material-symbols')
const kernel = new Kernel(version)
kernel.startExecutable('BootLoader', Permission.SYSTEM).catch(e => console.error(e))
})

View file

@ -1,6 +1,6 @@
import semver from 'semver' import semver from 'semver'
import Kernel from '../kernel' import Kernel from '../kernel'
import { Process, Executable, Package, Library, Permission, LoadedLibrary, LibraryPath } from '../types' import { Process, Executable, Package, Library, Permission, LoadedLibrary, LibraryPath, FileSystem } from '../types'
import FlowWindow from './FlowWindow' import FlowWindow from './FlowWindow'
import LibraryLib from './LibraryLib' import LibraryLib from './LibraryLib'
import ProcLib from './ProcLib' import ProcLib from './ProcLib'
@ -9,6 +9,7 @@ export default class ProcessLib {
readonly pid: number readonly pid: number
readonly token: string readonly token: string
process: Process process: Process
fs: FileSystem
private readonly _kernel: Kernel private readonly _kernel: Kernel
readonly kernel: { readonly kernel: {
getExecutable: (url: string) => Promise<Executable> getExecutable: (url: string) => Promise<Executable>
@ -17,8 +18,6 @@ export default class ProcessLib {
[key: string]: Package [key: string]: Package
} }
config: any config: any
setConfig: (data: any) => void
setFS: (fs: any) => void
} }
readonly permission: Permission readonly permission: Permission
@ -32,6 +31,8 @@ export default class ProcessLib {
} }
constructor (url: string, pid: number, token: string, permission = Permission.USER, data = {}, process: Process, kernel: Kernel) { constructor (url: string, pid: number, token: string, permission = Permission.USER, data = {}, process: Process, kernel: Kernel) {
if (kernel.fs === false) return
this.fs = kernel.fs
this.permission = permission this.permission = permission
this.pid = pid this.pid = pid
this.token = token this.token = token
@ -40,9 +41,7 @@ export default class ProcessLib {
getExecutable: kernel.getExecutable.bind(kernel), getExecutable: kernel.getExecutable.bind(kernel),
processList: kernel.processList, processList: kernel.processList,
packageList: kernel.packageList, packageList: kernel.packageList,
config: kernel.config, config: kernel.config
setConfig: (data: any) => kernel.setConfig(data, this),
setFS: (fs: any) => kernel.setFS(fs, this)
} }
this.process = process this.process = process
this.data = data this.data = data
@ -65,8 +64,7 @@ export default class ProcessLib {
const importedExecutable = (await module()) as any const importedExecutable = (await module()) as any
executable = importedExecutable.default executable = importedExecutable.default
} catch { } catch {
if (this._kernel.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.') const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
const dataURL = `data:text/javascript;base64,${Buffer.from(await this._kernel.fs.readFile(`/opt/${url}.js`)).toString('base64')}`
const importedExecutable = await import(dataURL) const importedExecutable = await import(dataURL)
executable = importedExecutable.default executable = importedExecutable.default
} }

View file

@ -1,14 +1,14 @@
import HTML from '../HTML' import HTML from '../HTML'
import { AppClosedEvent, AppOpenedEvent, Directory, FileSystemObject, Process } from '../types' import { AppClosedEvent, AppOpenedEvent, Directory, FileSystemObject, Process } from '../types'
import { getTime } from '../utils' import { getTime } from '../utils'
import { db, defaultFS, initializeDatabase, read, setFileSystem, write } from './lib/VirtualFS' import { defaultFS } from './VirtualFS'
import nullIcon from '../assets/icons/application-default-icon.svg' import nullIcon from '../assets/icons/application-default-icon.svg'
import { parse } from 'js-ini' import { parse } from 'js-ini'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
const BootLoader: Process = { const BootLoader: Process = {
config: { config: {
name: 'Bootloader', name: 'Desktop',
type: 'process', type: 'process',
targetVer: '1.0.0-indev.0' targetVer: '1.0.0-indev.0'
}, },
@ -17,58 +17,10 @@ const BootLoader: Process = {
const splashElement = splashScreen.getElement() const splashElement = splashScreen.getElement()
splashElement.appendTo(document.body) splashElement.appendTo(document.body)
const fs = await process.loadLibrary('lib/VirtualFS') const fs = process.fs
const wm = await process.loadLibrary('lib/WindowManager') const wm = await process.loadLibrary('lib/WindowManager')
const launcher = await process.loadLibrary('lib/Launcher') const launcher = await process.loadLibrary('lib/Launcher')
await initializeDatabase('virtualfs')
db.onerror = (event: Event) => {
const target = event.target as IDBRequest
const errorMessage = target.error !== null ? target.error.message : 'Unknown error'
throw new Error(`[VirtualFS] ${target.error?.name ?? 'Unknown Error'}: ${errorMessage}`)
}
if ('storage' in navigator) {
await navigator.storage?.persist()?.catch(e => console.error(e))
} else {
console.warn('Persistent storage is not supported.')
}
const fileSystem = await read() as FileSystemObject
if (fileSystem === undefined) {
await write(defaultFS)
} else {
const appsDirectory = ((fileSystem.root.children.home as Directory).children.Applications as Directory).children
const defaultAppsDirectory = ((defaultFS.root.children.home as Directory).children.Applications as Directory).children
for (const file in defaultAppsDirectory) {
if (appsDirectory[file] === undefined && defaultAppsDirectory[file] !== undefined) {
console.log(file)
appsDirectory[file] = defaultAppsDirectory[file]
}
}
await write(fileSystem)
await setFileSystem(fileSystem)
}
const config = Buffer.from(await fs.readFile('/etc/flow')).toString()
process.kernel.setFS(fs)
process.kernel.setConfig(parse(config))
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations()
for (const registration of registrations) {
await registration.unregister()
}
try {
await navigator.serviceWorker.register(`/uv-sw.js?url=${encodeURIComponent(btoa(process.kernel.config.SERVER))}&e=${uuid()}`, {
scope: '/service/'
})
} catch (e) {
console.error(e)
}
} else {
console.warn('Service workers are not supported.')
}
const input = new HTML('input').attr({ const input = new HTML('input').attr({
type: 'text', type: 'text',
placeholder: 'Search' placeholder: 'Search'

431
src/system/VirtualFS.ts Normal file
View file

@ -0,0 +1,431 @@
import { Directory, Errors, File, Permission, Stats } from '../types'
export const defaultFS: { root: Directory } = {
root: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {
home: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {
Downloads: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
},
Applications: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {
'Info.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Info')
},
'Manager.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Manager')
},
'Store.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Store')
},
'TaskManager.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/TaskManager')
},
'Browser.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Browser')
},
'ImageViewer.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/ImageViewer')
},
'Files.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Files')
},
'Editor.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Editor')
},
'Settings.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Settings')
}
}
},
Desktop: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {
'README.md': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('# Welcome to FlowOS!')
},
'Info.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Info.app')
},
'Manager.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Manager.app')
},
'Store.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Store.app')
},
'TaskManager.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/TaskManager.app')
},
'Browser.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Browser.app')
},
'ImageViewer.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/ImageViewer.app')
},
'Files.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Files.app')
},
'Editor.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Editor.app')
}
}
},
Pictures: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
},
Videos: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
},
Documents: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
},
Music: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
}
}
},
var: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {}
},
etc: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {
flow: {
type: 'file',
deleteable: false,
permission: Permission.ELEVATED,
content: Buffer.from([
'SERVER=https://server.flow-works.me',
'24HOUR=FALSE'
].join('\n'))
},
hostname: {
type: 'file',
deleteable: false,
permission: Permission.ELEVATED,
content: Buffer.from('flow')
}
}
},
opt: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {
apps: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {}
}
}
}
}
}
}
class VirtualFS {
private fileSystem: { root: Directory } = defaultFS
private db: IDBDatabase | null = null
async init (dbName = 'virtualfs'): Promise<VirtualFS> {
return await new Promise((resolve, reject) => {
indexedDB.deleteDatabase(dbName)
const request = indexedDB.open(dbName)
request.onerror = () => {
reject(new Error('Failed to open database'))
}
request.onsuccess = () => {
this.db = request.result
resolve(this)
}
request.onupgradeneeded = () => {
const db = request.result
db.createObjectStore('fs')
}
})
}
private setFileSystem (fileSystemObject: { root: Directory }): void {
this.fileSystem = fileSystemObject
}
private readonly read = async (): Promise<any> => {
const transaction = this.db?.transaction(['fs'], 'readonly')
const store = transaction?.objectStore('fs')
const getRequest = store?.get('fs')
return await new Promise((resolve, reject) => {
getRequest.onsuccess = () => {
resolve(getRequest.result)
}
getRequest.onerror = () => {
reject(getRequest.error)
}
})
}
private readonly write = async (fileSystemObject: { root: Directory }): Promise<void> => {
this.fileSystem = fileSystemObject
await this.save()
}
private readonly save = async (): Promise<void> => {
const transaction = this.db?.transaction(['fs'], 'readwrite')
const store = transaction?.objectStore('fs')
const putRequest = store?.put(this.fileSystem, 'fs')
return await new Promise((resolve, reject) => {
putRequest.onsuccess = () => {
document.dispatchEvent(new CustomEvent('fs_update', {}))
resolve()
}
putRequest.onerror = () => {
reject(putRequest.error)
}
})
}
private readonly navigatePath = async (path: string): Promise<{ current: Directory | File, parts: string[] }> => {
const parts = path.split('/').filter(x => x !== '')
let current = this.fileSystem.root
for (const part of parts) {
current = current.children[part] as Directory
}
return { current, parts }
}
private readonly navigatePathParent = async (path: string): Promise<{ current: Directory, parts: string[], filename: string }> => {
const parts = path.split('/').filter(x => x !== '')
const filename = parts.pop() as string
let current = this.fileSystem.root
for (const part of parts) {
current = current.children[part] as Directory
}
return { current, parts, filename }
}
private readonly handlePermissions = async (path: string): Promise<void> => {
const { current } = await this.navigatePath(path)
if (current.permission === Permission.SYSTEM) throw new Error(Errors.EPERM)
}
unlink = async (path: string): Promise<void> => {
const { current, filename } = await this.navigatePathParent(path)
if (!current.children[filename].deleteable) throw new Error(Errors.EPERM)
await this.handlePermissions(path)
Reflect.deleteProperty(current.children, filename)
console.debug(`unlink ${path}`)
await this.save()
}
readFile = async (path: string): Promise<Buffer> => {
const { current } = await this.navigatePath(path)
await this.handlePermissions(path)
if (current.type !== 'file') throw new Error(Errors.EISDIR)
console.debug(`read ${path}`)
return current.content
}
writeFile = async (path: string, content: string | Buffer): Promise<void> => {
const { current, filename } = await this.navigatePathParent(path)
let permission
if (typeof current.children[filename] === 'undefined') {
permission = Permission.USER
} else {
await this.handlePermissions(path)
permission = current.children[filename].permission
}
current.children[filename] = {
type: 'file',
deleteable: true,
permission,
content: Buffer.from(content)
}
console.debug(`write ${path}`)
await this.save()
}
mkdir = async (path: string): Promise<void> => {
const { current, filename } = await this.navigatePathParent(path)
let permission
if (typeof current.children[filename] === 'undefined') {
permission = Permission.USER
} else {
await this.handlePermissions(path)
permission = current.children[filename].permission
}
current.children[filename] = {
type: 'directory',
deleteable: true,
permission: path === '/tmp' ? Permission.USER : permission,
children: {}
}
console.debug(`mkdir ${path}`)
await this.save()
}
rmdir = async (path: string): Promise<void> => {
const { current, filename } = await this.navigatePathParent(path)
if (!current.deleteable) throw new Error(Errors.EPERM)
await this.handlePermissions(path)
if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR)
Reflect.deleteProperty(current.children, filename)
console.debug(`rmdir ${path}`)
await this.save()
}
readdir = async (path: string): Promise<string[]> => {
const { current } = await this.navigatePath(path)
if (current.type === 'file') throw new Error(Errors.ENOTDIR)
const result = await Promise.all(Object.keys(current.children ?? {}))
console.debug(`readdir ${path}`)
return result
}
stat = async (path: string): Promise<Stats> => {
const { current } = await this.navigatePath(path)
console.debug(`stat ${path}`)
return {
isDirectory: () => current.type === 'directory',
isFile: () => current.type === 'file'
}
}
rename = async (oldPath: string, newPath: string): Promise<void> => {
const { current: oldCurrent, filename: oldFilename } = await this.navigatePathParent(oldPath)
const { current: newCurrent, filename: newFilename } = await this.navigatePathParent(newPath)
if (!oldCurrent.deleteable) throw new Error(Errors.EPERM)
if (!newCurrent.deleteable) throw new Error(Errors.EPERM)
await this.handlePermissions(oldPath)
await this.handlePermissions(newPath)
newCurrent.children[newFilename] = oldCurrent.children[oldFilename]
Reflect.deleteProperty(oldCurrent.children, oldFilename)
console.debug(`rename ${oldPath} -> ${newPath}`)
await this.save()
}
exists = async (path: string): Promise<boolean> => {
console.debug(`exists ${path}`)
try {
const { current } = await this.navigatePath(path)
return current !== undefined
} catch (e) {
return false
}
}
}
export default VirtualFS

View file

@ -51,7 +51,7 @@ const Editor: Process = {
}, process) }, process)
}) })
const fs = await process.loadLibrary('lib/VirtualFS') const fs = process.fs
const data = process.data as EditorConfig const data = process.data as EditorConfig

View file

@ -18,7 +18,7 @@ const Files: Process = {
}, process) }, process)
}) })
const fs = await process.loadLibrary('lib/VirtualFS') const fs = process.fs
const MIMETypes = await process.loadLibrary('lib/MIMETypes') const MIMETypes = await process.loadLibrary('lib/MIMETypes')
win.content.style.display = 'flex' win.content.style.display = 'flex'

View file

@ -19,7 +19,7 @@ const ImageViewer: Process = {
}, process) }, process)
}) })
const fs = await process.loadLibrary('lib/VirtualFS') const fs = process.fs
const MIMETypes: Record<string, { type: string }> = await process.loadLibrary('lib/MIMETypes') const MIMETypes: Record<string, { type: string }> = await process.loadLibrary('lib/MIMETypes')
const HTML = await process.loadLibrary('lib/HTML') const HTML = await process.loadLibrary('lib/HTML')

View file

@ -24,7 +24,7 @@ const Settings: Process = {
) )
}) })
const fs = await process.loadLibrary('lib/VirtualFS') const fs = process.fs
const HTML = await process.loadLibrary('lib/HTML') const HTML = await process.loadLibrary('lib/HTML')
const { Input, Button } = await process.loadLibrary('lib/Components') const { Input, Button } = await process.loadLibrary('lib/Components')

View file

@ -19,7 +19,7 @@ const Store: Process = {
}, process) }, process)
}) })
const fs = await process.loadLibrary('lib/VirtualFS') const fs = process.fs
const HTML = await process.loadLibrary('lib/HTML') const HTML = await process.loadLibrary('lib/HTML')
const { Button, Icon } = await process.loadLibrary('lib/Components') const { Button, Icon } = await process.loadLibrary('lib/Components')

View file

@ -1,461 +0,0 @@
import Kernel from '../../kernel'
import ProcessLib from '../../structures/ProcessLib'
import { Directory, Errors, File, Library, Permission, Stats } from '../../types'
console.debug = (...args: any[]) => {
console.log('[VirtualFS]', ...args)
}
export const defaultFS: { root: Directory } = {
root: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {
home: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {
Downloads: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
},
Applications: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {
'Info.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Info')
},
'Manager.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Manager')
},
'Store.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Store')
},
'TaskManager.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/TaskManager')
},
'Browser.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Browser')
},
'ImageViewer.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/ImageViewer')
},
'Files.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Files')
},
'Editor.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Editor')
},
'Settings.app': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('apps/Settings')
}
}
},
Desktop: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {
'README.md': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('# Welcome to FlowOS!')
},
'Info.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Info.app')
},
'Manager.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Manager.app')
},
'Store.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Store.app')
},
'TaskManager.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/TaskManager.app')
},
'Browser.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Browser.app')
},
'ImageViewer.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/ImageViewer.app')
},
'Files.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Files.app')
},
'Editor.lnk': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from('/home/Applications/Editor.app')
}
}
},
Pictures: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
},
Videos: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
},
Documents: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
},
Music: {
type: 'directory',
deleteable: false,
permission: Permission.USER,
children: {}
}
}
},
var: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {}
},
etc: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {
flow: {
type: 'file',
deleteable: false,
permission: Permission.ELEVATED,
content: Buffer.from([
'SERVER=https://server.flow-works.me',
'24HOUR=FALSE'
].join('\n'))
},
hostname: {
type: 'file',
deleteable: false,
permission: Permission.ELEVATED,
content: Buffer.from('flow')
}
}
},
opt: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {
apps: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {}
}
}
}
}
}
}
export const setFileSystem = async (fileSystemObject: { root: Directory }): Promise<void> => {
fileSystem = fileSystemObject
}
export let db: IDBDatabase
let fileSystem: { root: Directory }
let kernel: Kernel
let process: ProcessLib
export const initializeDatabase = async (dbName: string): Promise<boolean> => {
return await new Promise((resolve, reject) => {
const request = window.indexedDB.open(dbName)
request.onupgradeneeded = (event: Event) => {
const target = event.target as IDBRequest
const db = target.result
db.createObjectStore('fs')
}
request.onerror = (event: Event) => {
reject(new Error('[VirtualFS] Error opening database.'))
}
request.onsuccess = () => {
db = request.result
resolve(true)
}
})
}
export const read = async (): Promise<any> => {
const transaction = db.transaction(['fs'], 'readonly')
const store = transaction.objectStore('fs')
const getRequest = store.get('fs')
return await new Promise((resolve, reject) => {
getRequest.onsuccess = () => {
resolve(getRequest.result)
}
getRequest.onerror = () => {
reject(getRequest.error)
}
})
}
export const write = async (fileSystemObject: { root: Directory }): Promise<void> => {
fileSystem = fileSystemObject
await save()
}
const save = async (): Promise<void> => {
const transaction = db.transaction(['fs'], 'readwrite')
const store = transaction.objectStore('fs')
const putRequest = store.put(fileSystem, 'fs')
return await new Promise((resolve, reject) => {
putRequest.onsuccess = () => {
document.dispatchEvent(new CustomEvent('fs_update', {}))
resolve()
}
putRequest.onerror = () => {
reject(putRequest.error)
}
})
}
const handlePermissions = async (path: string): Promise<void> => {
let { current } = (await navigatePath(path))
if (current === undefined) current = (await navigatePathParent(path)).current
if (current.permission === Permission.USER && current.permission > process.permission) {
const uac = await kernel.startExecutable('UserAccessControl', Permission.SYSTEM, { type: 'fs', process, path })
if (uac.value === false) {
throw new Error(Errors.EACCES)
}
}
if (current.permission === Permission.ELEVATED && current.permission > process.permission) {
const uac = await kernel.startExecutable('UserAccessControl', Permission.SYSTEM, { type: 'fs', process, path })
if (uac.value === false) {
throw new Error(Errors.EACCES)
}
}
if (current.permission === Permission.SYSTEM && current.permission > process.permission) throw new Error(Errors.EPERM)
}
const navigatePath = async (path: string): Promise<{ current: Directory | File, parts: string[] }> => {
const parts = path.split('/').filter(x => x !== '')
let current = fileSystem.root
for (const part of parts) {
current = current.children[part] as Directory
}
return { current, parts }
}
const navigatePathParent = async (path: string): Promise<{ current: Directory, parts: string[], filename: string }> => {
const parts = path.split('/').filter(x => x !== '')
const filename = parts.pop() as string
let current = fileSystem.root
for (const part of parts) {
current = current.children[part] as Directory
}
return { current, parts, filename }
}
const VirtualFS: Library = {
config: {
name: 'VirtualFS',
type: 'library',
targetVer: '1.0.0-indev.0'
},
init: (l, k, p) => {
kernel = k
process = p
},
data: {
unlink: async (path: string): Promise<void> => {
const { current, filename } = await navigatePathParent(path)
if (!current.children[filename].deleteable) throw new Error(Errors.EPERM)
await handlePermissions(path)
Reflect.deleteProperty(current.children, filename)
console.debug(`unlink ${path}`)
await save()
},
readFile: async (path: string): Promise<Buffer> => {
const { current } = await navigatePath(path)
await handlePermissions(path)
if (current.type !== 'file') throw new Error(Errors.EISDIR)
console.debug(`read ${path}`)
return current.content
},
writeFile: async (path: string, content: string | Buffer): Promise<void> => {
const { current, filename } = await navigatePathParent(path)
let permission
if (typeof current.children[filename] === 'undefined') {
permission = Permission.USER
} else {
await handlePermissions(path)
permission = current.children[filename].permission
}
current.children[filename] = {
type: 'file',
deleteable: true,
permission,
content: Buffer.from(content)
}
console.debug(`write ${path}`)
await save()
},
mkdir: async (path: string): Promise<void> => {
const { current, filename } = await navigatePathParent(path)
let permission
if (typeof current.children[filename] === 'undefined') {
permission = Permission.USER
} else {
await handlePermissions(path)
permission = current.children[filename].permission
}
current.children[filename] = {
type: 'directory',
deleteable: true,
permission,
children: {}
}
console.debug(`mkdir ${path}`)
await save()
},
rmdir: async (path: string): Promise<void> => {
const { current, filename } = await navigatePathParent(path)
if (!current.deleteable) throw new Error(Errors.EPERM)
await handlePermissions(path)
if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR)
Reflect.deleteProperty(current.children, filename)
console.debug(`rmdir ${path}`)
await save()
},
readdir: async (path: string): Promise<string[]> => {
const { current } = await navigatePath(path)
if (current.type === 'file') throw new Error(Errors.ENOTDIR)
const result = await Promise.all(Object.keys(current.children ?? {}))
console.debug(`readdir ${path}`)
return result
},
stat: async (path: string): Promise<Stats> => {
const { current } = await navigatePath(path)
console.debug(`stat ${path}`)
return {
isDirectory: () => current.type === 'directory',
isFile: () => current.type === 'file'
}
},
rename: async (oldPath: string, newPath: string): Promise<void> => {
const { current: oldCurrent, filename: oldFilename } = await navigatePathParent(oldPath)
const { current: newCurrent, filename: newFilename } = await navigatePathParent(newPath)
if (!oldCurrent.deleteable) throw new Error(Errors.EPERM)
if (!newCurrent.deleteable) throw new Error(Errors.EPERM)
await handlePermissions(oldPath)
await handlePermissions(newPath)
newCurrent.children[newFilename] = oldCurrent.children[oldFilename]
Reflect.deleteProperty(oldCurrent.children, oldFilename)
console.debug(`rename ${oldPath} -> ${newPath}`)
await save()
},
exists: async (path: string): Promise<boolean> => {
console.debug(`exists ${path}`)
try {
const { current } = await navigatePath(path)
return current !== undefined
} catch (e) {
return false
}
}
}
}
export default VirtualFS

View file

@ -3,7 +3,6 @@ import Kernel from './kernel'
import FlowWindow from './structures/FlowWindow' import FlowWindow from './structures/FlowWindow'
import LibraryLib from './structures/LibraryLib' import LibraryLib from './structures/LibraryLib'
import ProcessLib from './structures/ProcessLib' import ProcessLib from './structures/ProcessLib'
import Components from './system/lib/Components'
import MIMETypes from './system/lib/MIMETypes' import MIMETypes from './system/lib/MIMETypes'
export interface AppClosedEvent extends CustomEvent { export interface AppClosedEvent extends CustomEvent {
@ -173,15 +172,20 @@ export interface StatusBar {
updateIcon: (ms: number) => void updateIcon: (ms: number) => void
} }
export interface IComponents {
[key: string]: {
new: (...args: any[]) => InstanceType<typeof HTML>
}
}
export type LoadedLibrary<T> = export type LoadedLibrary<T> =
T extends 'lib/VirtualFS' ? FileSystem :
T extends 'lib/WindowManager' ? WindowManager : T extends 'lib/WindowManager' ? WindowManager :
T extends 'lib/HTML' ? typeof HTML : T extends 'lib/HTML' ? typeof HTML :
T extends 'lib/Launcher' ? Launcher : T extends 'lib/Launcher' ? Launcher :
T extends 'lib/XOR' ? XOR : T extends 'lib/XOR' ? XOR :
T extends 'lib/StatusBar' ? StatusBar : T extends 'lib/StatusBar' ? StatusBar :
T extends 'lib/MIMETypes' ? typeof MIMETypes.data : T extends 'lib/MIMETypes' ? typeof MIMETypes.data :
T extends 'lib/Components' ? typeof Components.data : T extends 'lib/Components' ? IComponents :
any any
export type LibraryPath = 'lib/VirtualFS' | 'lib/WindowManager' | string export type LibraryPath = 'lib/VirtualFS' | 'lib/WindowManager' | string