diff options
Diffstat (limited to 'src/main')
-rw-r--r-- | src/main/kotlin/WebOS.kt | 366 | ||||
-rw-r--r-- | src/main/kotlin/asyncio.kt | 61 | ||||
-rw-r--r-- | src/main/kotlin/io/Path.kt | 78 | ||||
-rw-r--r-- | src/main/kotlin/io/files.kt | 203 | ||||
-rw-r--r-- | src/main/kotlin/util/sequence.kt | 5 | ||||
-rw-r--r-- | src/main/resources/index.html | 17 |
6 files changed, 730 insertions, 0 deletions
diff --git a/src/main/kotlin/WebOS.kt b/src/main/kotlin/WebOS.kt new file mode 100644 index 0000000..5bb788c --- /dev/null +++ b/src/main/kotlin/WebOS.kt @@ -0,0 +1,366 @@ +import io.FileService +import io.Path +import io.PrimitiveFileService +import io.PrimitiveINode +import kotlinx.browser.document +import kotlinx.browser.window +import org.w3c.dom.Element +import org.w3c.dom.Node +import org.w3c.dom.asList +import org.w3c.dom.events.Event +import org.w3c.dom.events.KeyboardEvent +import kotlin.properties.Delegates + +fun main() { + console.log("Hello from Kotlin") + + val runner = IORunner.runOnIO { + println("ON START") + wait() + println("WAIT CALLED ONCE") + val x = getOneKey() + println("INPUT GET: $x") + println("EXITING NOW") + } + println("runOnIO called -> CALLING WHATEVER") + runner._CALLED_BY_JS_onWhatever() + println("CALLED WHATEVER -> CALLING INPUT") + runner._CALLED_BY_JS_onInput(window.prompt("INPUT") ?: "NO INPUT") + println("CALLED INPUT") + return + + + + + + + val webos = WebOS() + document.body?.addEventListener("load", { + document.body?.querySelectorAll(".webosconsole")?.asList()?.forEach { + if (it !is Element) return@forEach + webos.registerConsole(it) + } + }) +} + +data class CharacterRun(val text: String, val color: String) + +abstract class Activity(val console: Console) { + + open fun handleKeyPress(key: Key) {} + open fun update() {} + abstract fun render(columns: Int, rows: Int): List<List<CharacterRun>> +} + +class ShellRunner(console: Console) : Activity(console) { + + val history = mutableListOf<String>() + + val shellProgramStack = ArrayDeque<ShellProgram>() + + var lastLine = "" + + fun newLine() { + history.add(lastLine) + lastLine = "" + console.invalidateRender() + } + + fun println(it: String = "") { + print(it) + newLine() + } + + fun print(it: String) { + lastLine += it + console.invalidateRender() + } + + override fun update() { + shellProgramStack.lastOrNull()?.update() + } + + fun getInput(): String? { + if (inputBuffer.contains("\n")) { + val r = inputBuffer.substringBefore("\n") + inputBuffer = inputBuffer.substringAfter("\n") + return r + } + return null + } + + override fun render(columns: Int, rows: Int): List<List<CharacterRun>> = + history.map { listOf(CharacterRun(it.take(columns), "white")) }.takeLast(rows - 1) + listOf( + listOf( + CharacterRun( + lastLine + inputBuffer, + "white" + ) + ) + ) + + var inputBuffer = "" + + override fun handleKeyPress(key: Key) = when (key) { + is Key.Printable -> { + inputBuffer += key.char + } + is Key.Enter -> { + inputBuffer += "\n" + } + else -> Unit + } + + fun <T : ShellProgram> openShellProgram(program: (ShellRunner) -> T): ShellRunner = openShellProgram(program(this)) + + fun <T : ShellProgram> openShellProgram(program: T): ShellRunner { + shellProgramStack.addLast(program) + return this + } + +} + +abstract class ShellProgram(val shellRunner: ShellRunner) { + + abstract fun update(): Unit +} + +sealed class Key { + object Alt : Key() + object AltGraph : Key() + object CapsLock : Key() + object Control : Key() + object Function : Key() + object FunctionLock : Key() + object Hyper : Key() + object Meta : Key() + object NumLock : Key() + object Shift : Key() + object Super : Key() + object Symbol : Key() + object SymbolLock : Key() + object Enter : Key() + object Tab : Key() + sealed class Arrow : Key() { + object Down : Key() + object Left : Key() + object Right : Key() + object Up : Key() + } + + object End : Key() + object Home : Key() + object PageUp : Key() + object PageDown : Key() + object Backspace : Key() + object Clear : Key() + object Copy : Key() + object CrSel : Key() + object Cut : Key() + object Delete : Key() + object EraseEof : Key() + object ExSel : Key() + object Insert : Key() + object Paste : Key() + object Redo : Key() + object Undo : Key() + object Accept : Key() + object Again : Key() + object Attn : Key() + object Cancel : Key() + object ContextMenu : Key() + object Escape : Key() + object Execute : Key() + object Find : Key() + object Finish : Key() + object Help : Key() + class Printable(val char: Char) : Key() + class FunctionN(val n: Int) : Key() + companion object { + + fun from(string: String) = when (string) { + "Alt" -> Alt + "AltGraph" -> AltGraph + "CapsLock" -> CapsLock + "Control" -> Control + "Fn" -> Function + "FnLock" -> FunctionLock + "Hyper" -> Hyper + "Meta" -> Meta + "NumLock" -> NumLock + "Shift" -> Shift + "Super" -> Super + "Symbol" -> Symbol + "SymbolLock" -> SymbolLock + "Enter" -> Enter + "Tab" -> Tab + "Down" -> Arrow.Down + "Left" -> Arrow.Left + "Right" -> Arrow.Right + "Up" -> Arrow.Up + "End" -> End + "Home" -> Home + "PageUp" -> PageUp + "PageDown" -> PageDown + "Backspace" -> Backspace + "Clear" -> Clear + "Copy" -> Copy + "CrSel" -> CrSel + "Cut" -> Cut + "Delete" -> Delete + "EraseEof" -> EraseEof + "ExSel" -> ExSel + "Insert" -> Insert + "Paste" -> Paste + "Redo" -> Redo + "Undo" -> Undo + "Accept" -> Accept + "Again" -> Again + "Attn" -> Attn + "Cancel" -> Cancel + "ContextMenu" -> ContextMenu + "Escape" -> Escape + "Execute" -> Execute + "Find" -> Find + "Finish" -> Finish + "Help" -> Help + else -> if (string.length == 1) + Printable(string.first()) + else if (string.first() == 'F') + FunctionN(string.substring(1).toInt()) + else throw TODO() + } + } +} + +class Login(shellRunner: ShellRunner) : ShellProgram(shellRunner) { + + override fun update() { + while (true) + when (state) { + 0 -> { + shellRunner.print("Username: ") + state = 1 + } + 1 -> { + val inp = shellRunner.getInput() ?: return + shellRunner.println(inp) + shellRunner.print("Password: ") + username = inp + state = 2 + } + 2 -> { + val inp = shellRunner.getInput() ?: return + shellRunner.println(inp) + shellRunner.println("Login complete)") + password = inp + // TODO: check password and set user + state = 3 + } + 3 -> { + TODO() + } + } + + } + + var state = 0 + var username = "" + var password = "" + +} + +class Console(val os: WebOS, val renderElement: Element?) { + + val isVirtual get() = renderElement == null + val activityStack = ArrayDeque<Activity>() + + var columns: Int = 80 + var rows: Int = 46 + + var shouldRerender = true + + var currentUser: User? = null + + private var _workingDirectory: Path.Absolute? = null + + var workingDirectory: Path.Absolute + get() = _workingDirectory ?: currentUser?.homeDirectory ?: Path.root + set(value) { + _workingDirectory = value + } + + init { + renderElement?.addEventListener("keydown", ::computeKeypressEvent) + } + + fun computeKeypressEvent(event: Event) { + if (!isInFocus()) return + if (event !is KeyboardEvent) return + activityStack.lastOrNull()?.handleKeyPress(Key.from(event.key)) + } + + private fun isInFocus(): Boolean = renderElement.containsOrIs(document.activeElement) + + private fun Node?.containsOrIs(node: Node?) = this == node || this?.contains(node) ?: false + + fun <T : Activity> openActivity(activity: (Console) -> T): Console = openActivity(activity(this)) + + fun <T : Activity> openActivity(activity: T): Console { + activityStack.addLast(activity) + invalidateRender() + return this + } + + fun render() { + if (renderElement == null) return + if (!shouldRerender) return + shouldRerender = false + val x = activityStack.lastOrNull()?.render(columns, rows) + console.log(x) + } + + fun invalidateRender() { + shouldRerender = true + window.requestAnimationFrame { render() } + } + + fun resize(newColumns: Int, newRows: Int) { + invalidateRender() + } + + // TODO: Handle resizes of the renderElement + + fun update(): Unit { + activityStack.lastOrNull()?.update() + } + + var updateInterval by Delegates.notNull<Int>() + + fun start(): Console { + updateInterval = window.setInterval(::update, 10) + return openActivity(ShellRunner(this).also { it.openShellProgram(Login(it)) }) + } + + fun destroy() { + window.clearInterval(updateInterval) + } + +} + +class WebOS { + + val hostname: String = "host" + private val _consoles = mutableListOf<Console>() + val consoles get() = _consoles.toList() + val files: FileService<PrimitiveINode> = PrimitiveFileService() + fun registerConsole(element: Element) { + _consoles.add(Console(this, element)) + } +} + +data class User( + val name: String, + val homeDirectory: Path.Absolute, + val isRoot: Boolean = false, +) diff --git a/src/main/kotlin/asyncio.kt b/src/main/kotlin/asyncio.kt new file mode 100644 index 0000000..e14fea9 --- /dev/null +++ b/src/main/kotlin/asyncio.kt @@ -0,0 +1,61 @@ +import kotlin.coroutines.* + +class IORunner : Continuation<Unit> { + var nextStep: Continuation<Unit>? = null + val eventQueue = ArrayDeque<String>() + fun _CALLED_BY_JS_onInput(str: String) { + eventQueue.addLast(str) + awake() + } + + fun _CALLED_BY_JS_onWhatever() { + awake() + } + + private fun awake() { + val step = nextStep + if (step != null) { + nextStep = null + step.resume(Unit) + } + } + + suspend fun wait() { + return suspendCoroutine { c -> + console.log("SUSPENDING COROUTINE") + nextStep = c + } + } + + override val context: CoroutineContext + get() = EmptyCoroutineContext + + override fun resumeWith(result: Result<Unit>) { + if (result.isFailure) { + result.exceptionOrNull()?.printStackTrace() + } else { + println("IORunner exited successfully") + } + } + + companion object { + fun runOnIO(block: suspend IORunner.() -> Unit): IORunner { + val r = IORunner() + block.startCoroutine(r, r) + return r + } + } +} + + +suspend fun IORunner.getOneKey(): String { + while (true) { + val x = eventQueue.removeFirstOrNull() + if (x == null) + wait() + else { + return x + } + } +} + diff --git a/src/main/kotlin/io/Path.kt b/src/main/kotlin/io/Path.kt new file mode 100644 index 0000000..241c559 --- /dev/null +++ b/src/main/kotlin/io/Path.kt @@ -0,0 +1,78 @@ +package io + +sealed class Path { + abstract val parts: List<String> + fun toAbsolutePath(relativeTo: Absolute): Absolute { + return relativeTo.resolve(this) + } + + abstract fun resolve(path: Path): Path + + abstract val stringPath: String + + companion object { + val root = Absolute(listOf()) + + fun ofShell(string: String, userHome: Absolute): Path = + ofShell(string.split("/"), userHome) + + fun ofShell(vararg parts: String, userHome: Absolute): Path = + ofShell(parts.toList(), userHome) + + fun of(vararg parts: String): Path = + of(parts.toList()) + + fun of(string: String): Path = + of(string.split("/")) + + fun ofShell(parts: List<String>, userHome: Absolute): Path { + if (parts.firstOrNull() == "~") + return userHome.resolve(Relative(parts.subList(1, parts.size).filter { it.isNotEmpty() })) + return of(parts) + } + + fun of(parts: List<String>): Path { + if (parts.isEmpty()) + return root + if (parts[0] == "") // Starts with a / + return Absolute(parts.subList(1, parts.size).filter { it.isNotEmpty() }) + return Relative(parts.filter { it.isNotEmpty() }) + } + } + + data class Relative internal constructor(override val parts: List<String>) : Path() { + override fun resolve(path: Path): Path { + if (path is Absolute) return path + return Relative(this.parts + path.parts) + } + + override val stringPath: String get() = parts.joinToString("/") + } + + data class Absolute internal constructor(override val parts: List<String>) : Path() { + override fun resolve(path: Path): Absolute { + if (path is Absolute) return path + return Absolute(this.parts + path.parts) + } + + override val stringPath: String get() = "/" + parts.joinToString("/") + + fun relativize(path: Path): Relative = when (path) { + is Relative -> path + is Absolute -> { + var idx = 0 + while (idx < path.parts.size && idx < parts.size && path.parts[idx] == parts[idx]) { + idx++ + } + val returns = if (idx < parts.size) { + parts.size - idx + } else { + 0 + } + Relative(List(returns) { ".." } + path.parts.subList(idx, path.parts.size)) + } + } + } + + override fun toString(): String = "Path($stringPath)" +} diff --git a/src/main/kotlin/io/files.kt b/src/main/kotlin/io/files.kt new file mode 100644 index 0000000..4c53c4e --- /dev/null +++ b/src/main/kotlin/io/files.kt @@ -0,0 +1,203 @@ +package io + +import User + +sealed interface CreateFileResult { + object Created : CreateFileResult + sealed interface Failure : CreateFileResult { + object NoPermission : Failure + object NoParent : Failure + object AlreadyExists : Failure + } +} + +sealed interface WriteFileResult { + object Written : WriteFileResult + sealed interface Failure : WriteFileResult { + object NoPermission : Failure + object NotAFile : Failure + data class NotEnoughSpace( + val dataSize: Long, + val spaceLeft: Long + ) : Failure + } +} + +sealed interface ReadFileResult { + data class Read(val data: ByteArray) : ReadFileResult { + override fun equals(other: Any?): Boolean = (other as? Read)?.let { it.data.contentEquals(this.data) } ?: false + + override fun hashCode(): Int { + return data.contentHashCode() + } + } + + sealed interface Failure : ReadFileResult { + object NoPermission : Failure + object NotAFile : Failure + } +} + +sealed interface DeleteFileResult { + sealed interface Failure : DeleteFileResult { + object NoPermission : Failure + object NotAFile : Failure + } + + object Deleted : DeleteFileResult +} + +interface FileService<INode> { + fun getPath(iNode: INode): Path.Absolute + fun getINode(path: Path.Absolute): INode + fun createFile(iNode: INode, user: User): CreateFileResult + fun createSymlink(iNode: INode, user: User, path: Path): CreateFileResult + fun createDirectory(iNode: INode, user: User): CreateFileResult + fun writeToFile(iNode: INode, user: User, data: ByteArray): WriteFileResult + fun readFromFile(iNode: INode, user: User): ReadFileResult + fun deleteFile(iNode: INode, user: User): DeleteFileResult + fun exists(iNode: INode): Boolean + fun isFile(iNode: INode): Boolean + fun isDirectory(iNode: INode): Boolean + fun isSymlink(iNode: INode): Boolean + fun resolve(iNode: INode, fragment: String): INode + fun resolve(iNode: INode, path: Path.Relative): INode + fun changePermissions(iNode: INode, user: User, permissionUpdate: Map<User, Permission>) +} + +data class Permission( + val read: Boolean, + val write: Boolean, + val execute: Boolean +) { + companion object { + val default get() = Permission(read = true, write = true, execute = false) + } +} + +sealed interface PrimitiveStorageBlob { + val permissions: MutableMap<User, Permission> + + class File(var data: ByteArray, override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob + class Symlink(val path: Path, override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob + class Directory(override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob +} + +data class PrimitiveINode internal constructor(internal val internalPath: String) +class PrimitiveFileService : FileService<PrimitiveINode> { + private val storageBlobs = mutableMapOf<String, PrimitiveStorageBlob>( + "/" to PrimitiveStorageBlob.Directory(mutableMapOf()) + ) + + override fun getPath(iNode: PrimitiveINode): Path.Absolute = Path.of(iNode.internalPath) as Path.Absolute + + override fun getINode(path: Path.Absolute): PrimitiveINode { + return resolve(PrimitiveINode("/"), Path.Relative(path.parts)) + } + + override fun resolve(iNode: PrimitiveINode, fragment: String): PrimitiveINode { + if (fragment == "..") { + val up = iNode.internalPath.substringBeforeLast('/') + if (up.isEmpty()) return PrimitiveINode("/") + return PrimitiveINode(up) + } + if (fragment.isEmpty() || fragment == ".") + return iNode + val blob = storageBlobs[iNode.internalPath] + return when (blob) { + is PrimitiveStorageBlob.Symlink -> { + when (blob.path) { + is Path.Absolute -> getINode(blob.path) + is Path.Relative -> resolve(resolve(iNode, ".."), blob.path) + } + } + else -> { + PrimitiveINode(iNode.internalPath + "/" + fragment) + } + } + } + + override fun resolve(iNode: PrimitiveINode, path: Path.Relative): PrimitiveINode = + path.parts.fold(iNode) { node, fragment -> resolve(node, fragment) } + + private fun getStorageBlob(iNode: PrimitiveINode): PrimitiveStorageBlob? = storageBlobs[iNode.internalPath] + + override fun writeToFile(iNode: PrimitiveINode, user: User, data: ByteArray): WriteFileResult { + val file = getStorageBlob(iNode) as? PrimitiveStorageBlob.File ?: return WriteFileResult.Failure.NotAFile + if (!hasPermission(user, file) { read }) + return WriteFileResult.Failure.NoPermission + file.data = data + return WriteFileResult.Written + } + + override fun readFromFile(iNode: PrimitiveINode, user: User): ReadFileResult { + val file = getStorageBlob(iNode) as? PrimitiveStorageBlob.File ?: return ReadFileResult.Failure.NotAFile + if (!hasPermission(user, file) { read }) + return ReadFileResult.Failure.NoPermission + return ReadFileResult.Read(file.data) + } + + override fun exists(iNode: PrimitiveINode): Boolean = + getStorageBlob(iNode) != null + + override fun isFile(iNode: PrimitiveINode): Boolean = + getStorageBlob(iNode) is PrimitiveStorageBlob.File + + override fun isDirectory(iNode: PrimitiveINode): Boolean = + getStorageBlob(iNode) is PrimitiveStorageBlob.Directory + + override fun isSymlink(iNode: PrimitiveINode): Boolean = + getStorageBlob(iNode) is PrimitiveStorageBlob.Symlink + + override fun changePermissions(iNode: PrimitiveINode, user: User, permissionUpdate: Map<User, Permission>) { + val file = getStorageBlob(iNode) ?: return // TODO Results + if (!hasPermission(user, file) { write }) + return // TODO Results + file.permissions.putAll(permissionUpdate) + return // TODO Results + } + + override fun deleteFile(iNode: PrimitiveINode, user: User): DeleteFileResult { + val file = getStorageBlob(iNode) ?: return DeleteFileResult.Failure.NotAFile + if (!hasPermission(user, file) { write }) + return DeleteFileResult.Failure.NoPermission + (storageBlobs.keys.filter { it.startsWith(iNode.internalPath + "/") } + listOf(iNode.internalPath)).forEach { + storageBlobs.remove(it) + } + return DeleteFileResult.Deleted + } + + private fun hasPermission(user: User, blob: PrimitiveStorageBlob, check: Permission.() -> Boolean): Boolean { + return user.isRoot || blob.permissions[user]?.let(check) ?: false + } + + private fun checkCreationPreconditions(iNode: PrimitiveINode, user: User): CreateFileResult? { + if (storageBlobs.containsKey(iNode.internalPath)) return CreateFileResult.Failure.AlreadyExists + val parent = getStorageBlob(resolve(iNode, "..")) + if (parent !is PrimitiveStorageBlob.Directory) return CreateFileResult.Failure.NoParent + if (!hasPermission(user, parent) { write }) return CreateFileResult.Failure.NoPermission + return null + } + + override fun createFile(iNode: PrimitiveINode, user: User): CreateFileResult { + val preconditions = checkCreationPreconditions(iNode, user) + if (preconditions != null) return preconditions + storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.File(byteArrayOf(), mutableMapOf(user to Permission.default)) + return CreateFileResult.Created + } + + override fun createSymlink(iNode: PrimitiveINode, user: User, path: Path): CreateFileResult { + val preconditions = checkCreationPreconditions(iNode, user) + if (preconditions != null) return preconditions + storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.Symlink(path, mutableMapOf(user to Permission.default)) + return CreateFileResult.Created + } + + override fun createDirectory(iNode: PrimitiveINode, user: User): CreateFileResult { + val preconditions = checkCreationPreconditions(iNode, user) + if (preconditions != null) return preconditions + storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.Directory(mutableMapOf(user to Permission.default)) + return CreateFileResult.Created + } + +} diff --git a/src/main/kotlin/util/sequence.kt b/src/main/kotlin/util/sequence.kt new file mode 100644 index 0000000..72b07dc --- /dev/null +++ b/src/main/kotlin/util/sequence.kt @@ -0,0 +1,5 @@ +package util + +fun <T> Iterable<T>.expandWith(t: T): Sequence<T> = + this.asSequence() + generateSequence { t }.asSequence() + diff --git a/src/main/resources/index.html b/src/main/resources/index.html new file mode 100644 index 0000000..562ee45 --- /dev/null +++ b/src/main/resources/index.html @@ -0,0 +1,17 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" + content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <title>WebOs</title> +</head> +<body> +<noscript>tf you don't have js enabled? do you still live in the stone ages or what?</noscript> +<div id="content"> +<div class="webosconsole"></div> +</div> +<script src="/webos.js"></script> +</body> +</html> |