diff --git a/.env b/.env
new file mode 100644
index 0000000..d5fbc82
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+GOOS=js
+GOARCH=wasm
diff --git a/Makefile b/Makefile
index 77ce405..cdebda1 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
SHELL := /usr/bin/env bash
-GO_VERSION = 1.16.6
+GO_VERSION = 1.16
GOROOT =
PATH := ${PWD}/cache/go/bin:${PWD}/cache/go/misc/wasm:${PATH}
GOOS = js
@@ -7,6 +7,8 @@ GOARCH = wasm
export
LINT_VERSION=1.27.0
+BUILD_FLAGS = -trimpath
+
.PHONY: serve
serve:
go run ./server
@@ -71,7 +73,7 @@ cache/go${GO_VERSION}: cache
git clone \
--depth 1 \
--single-branch \
- --branch hackpad-go${GO_VERSION} \
+ --branch hackpad/release-branch.go${GO_VERSION} \
https://github.com/hack-pad/go.git \
"$$TMP"; \
pushd "$$TMP/src"; \
@@ -91,10 +93,10 @@ cache/go${GO_VERSION}: cache
touch cache/go.mod # Makes it so linters will ignore this dir
server/public/wasm/%.wasm: server/public/wasm go
- go build -o $@ ./cmd/$*
+ go build ${BUILD_FLAGS} -o $@ ./cmd/$*
server/public/wasm/main.wasm: server/public/wasm go
- go build -o server/public/wasm/main.wasm .
+ go build ${BUILD_FLAGS} -o server/public/wasm/main.wasm .
server/public/wasm/wasm_exec.js: go
cp cache/go/misc/wasm/wasm_exec.js server/public/wasm/wasm_exec.js
diff --git a/cmd/editor/dom/element.go b/cmd/editor/dom/element.go
index d6f20aa..7240523 100644
--- a/cmd/editor/dom/element.go
+++ b/cmd/editor/dom/element.go
@@ -8,6 +8,7 @@ import (
"github.com/hack-pad/hackpad/internal/common"
"github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
"github.com/hack-pad/hackpad/internal/log"
)
@@ -112,7 +113,7 @@ func (e *Element) QuerySelectorAll(query string) []*Element {
}
func (e *Element) AddEventListener(name string, listener EventListener) {
- e.elem.Call("addEventListener", name, js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ e.elem.Call("addEventListener", name, jsfunc.NonBlocking(func(this js.Value, args []js.Value) interface{} {
defer common.CatchExceptionHandler(func(err error) {
log.Error("recovered from panic: ", err, "\n", string(debug.Stack()))
})
diff --git a/cmd/editor/dom/window.go b/cmd/editor/dom/window.go
index 35f92fb..c2ed546 100644
--- a/cmd/editor/dom/window.go
+++ b/cmd/editor/dom/window.go
@@ -7,6 +7,7 @@ import (
"time"
"github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
)
var (
@@ -15,8 +16,8 @@ var (
func SetTimeout(fn func(args []js.Value), delay time.Duration, args ...js.Value) int {
intArgs := append([]interface{}{
- interop.SingleUseFunc(func(_ js.Value, args []js.Value) interface{} {
- fn(args)
+ jsfunc.SingleUse(func(_ js.Value, args []js.Value) interface{} {
+ go fn(args)
return nil
}),
delay.Milliseconds(),
@@ -28,8 +29,8 @@ func SetTimeout(fn func(args []js.Value), delay time.Duration, args ...js.Value)
func QueueMicrotask(fn func()) {
queueMicrotask := window.GetProperty("queueMicrotask")
if queueMicrotask.Truthy() {
- queueMicrotask.Invoke(interop.SingleUseFunc(func(this js.Value, args []js.Value) interface{} {
- fn()
+ queueMicrotask.Invoke(jsfunc.SingleUse(func(this js.Value, args []js.Value) interface{} {
+ go fn()
return nil
}))
} else {
diff --git a/cmd/editor/editor.go b/cmd/editor/editor.go
index 1cd268b..9928c8d 100644
--- a/cmd/editor/editor.go
+++ b/cmd/editor/editor.go
@@ -1,3 +1,4 @@
+//go:build js
// +build js
package main
@@ -9,6 +10,7 @@ import (
"github.com/hack-pad/hackpad/cmd/editor/dom"
"github.com/hack-pad/hackpad/cmd/editor/ide"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
"github.com/hack-pad/hackpad/internal/log"
)
@@ -25,7 +27,7 @@ func (e editorJSFunc) New(elem *dom.Element) ide.Editor {
editor := &jsEditor{
titleChan: make(chan string, 1),
}
- editor.elem = js.Value(e).Invoke(elem, js.FuncOf(editor.onEdit))
+ editor.elem = js.Value(e).Invoke(elem, jsfunc.NonBlocking(editor.onEdit))
return editor
}
@@ -36,18 +38,16 @@ type jsEditor struct {
}
func (j *jsEditor) onEdit(js.Value, []js.Value) interface{} {
- go func() {
- contents := j.elem.Call("getContents").String()
- perm := os.FileMode(0700)
- info, err := os.Stat(j.filePath)
- if err == nil {
- perm = info.Mode()
- }
- err = ioutil.WriteFile(j.filePath, []byte(contents), perm)
- if err != nil {
- log.Error("Failed to write file contents: ", err)
- }
- }()
+ contents := j.elem.Call("getContents").String()
+ perm := os.FileMode(0700)
+ info, err := os.Stat(j.filePath)
+ if err == nil {
+ perm = info.Mode()
+ }
+ err = ioutil.WriteFile(j.filePath, []byte(contents), perm)
+ if err != nil {
+ log.Error("Failed to write file contents: ", err)
+ }
return nil
}
diff --git a/cmd/editor/main.go b/cmd/editor/main.go
index 7c0c828..86979d2 100644
--- a/cmd/editor/main.go
+++ b/cmd/editor/main.go
@@ -6,6 +6,7 @@ import (
"flag"
"io/ioutil"
"os"
+ "runtime/debug"
"syscall/js"
"github.com/hack-pad/hackpad/cmd/editor/dom"
@@ -13,7 +14,9 @@ import (
"github.com/hack-pad/hackpad/cmd/editor/plaineditor"
"github.com/hack-pad/hackpad/cmd/editor/taskconsole"
"github.com/hack-pad/hackpad/cmd/editor/terminal"
+ "github.com/hack-pad/hackpad/internal/common"
"github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
"github.com/hack-pad/hackpad/internal/log"
)
@@ -22,6 +25,11 @@ const (
)
func main() {
+ defer common.CatchExceptionHandler(func(err error) {
+ log.Error("Editor panic:", err, "\n", string(debug.Stack()))
+ os.Exit(1)
+ })
+
editorID := flag.String("editor", "", "Editor element ID to attach")
flag.Parse()
@@ -33,7 +41,7 @@ func main() {
app := dom.GetDocument().GetElementByID(*editorID)
app.AddClass("ide")
globalEditorProps := js.Global().Get("editor")
- globalEditorProps.Set("profile", js.FuncOf(interop.ProfileJS))
+ globalEditorProps.Set("profile", jsfunc.NonBlocking(interop.ProfileJS))
newEditor := globalEditorProps.Get("newEditor")
var editorBuilder ide.EditorBuilder = editorJSFunc(newEditor)
if !newEditor.Truthy() {
diff --git a/cmd/editor/taskconsole/console.go b/cmd/editor/taskconsole/console.go
index f8908b8..c343a55 100644
--- a/cmd/editor/taskconsole/console.go
+++ b/cmd/editor/taskconsole/console.go
@@ -78,7 +78,7 @@ func (c *console) runLoopIter() {
defer cancel(commandErr)
elapsed := time.Since(startTime)
if commandErr != nil {
- _, _ = c.stderr.Write([]byte(commandErr.Error()))
+ _, _ = c.stderr.Write([]byte(commandErr.Error() + "\n"))
}
exitCode := 0
diff --git a/cmd/editor/terminal/terminal.go b/cmd/editor/terminal/terminal.go
index abb485d..74488c0 100644
--- a/cmd/editor/terminal/terminal.go
+++ b/cmd/editor/terminal/terminal.go
@@ -10,6 +10,7 @@ import (
"github.com/hack-pad/hackpad/cmd/editor/dom"
"github.com/hack-pad/hackpad/cmd/editor/ide"
"github.com/hack-pad/hackpad/internal/common"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
"github.com/hack-pad/hackpad/internal/log"
"github.com/hack-pad/hackpadfs/indexeddb/idbblob"
"github.com/hack-pad/hackpadfs/keyvalue/blob"
@@ -74,7 +75,7 @@ func (t *terminal) start(rawName, name string, args ...string) error {
return err
}
- f := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ f := jsfunc.NonBlocking(func(this js.Value, args []js.Value) interface{} {
chunk := []byte(args[0].String())
_, err := stdin.Write(chunk)
if err == io.EOF {
diff --git a/cmd/worker/main.go b/cmd/worker/main.go
new file mode 100644
index 0000000..9496449
--- /dev/null
+++ b/cmd/worker/main.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+ "context"
+ "os"
+ "runtime/debug"
+
+ "github.com/hack-pad/go-indexeddb/idb"
+ "github.com/hack-pad/hackpad/internal/common"
+ "github.com/hack-pad/hackpad/internal/fs"
+ "github.com/hack-pad/hackpad/internal/jsworker"
+ "github.com/hack-pad/hackpad/internal/log"
+ "github.com/hack-pad/hackpad/internal/worker"
+ "github.com/hack-pad/hackpadfs/indexeddb"
+)
+
+func main() {
+ defer common.CatchExceptionHandler(func(err error) {
+ log.Errorf("Worker panicked: %+v", err)
+ log.Error(string(debug.Stack()))
+ os.Exit(1)
+ })
+
+ bootCtx := context.Background()
+ log.Debug("booting worker")
+ local, err := worker.NewLocal(bootCtx, jsworker.GetLocal())
+ if err != nil {
+ panic(err)
+ }
+ log.Debug("worker inited")
+ if err := setUpFS(); err != nil {
+ panic(err)
+ }
+ log.Debug("fs is setup")
+ if err := local.Start(); err != nil {
+ panic(err)
+ }
+ log.Debug("worker starting...")
+ <-local.Started()
+ pid := local.PID()
+ log.Debug("worker process started PID ", pid)
+ exitCode, err := local.Wait(pid)
+ if err != nil {
+ log.Error("Failed to wait for PID ", pid, ":", err)
+ exitCode = 1
+ }
+ log.Debug("worker stopped for PID ", pid, "; exit code = ", exitCode)
+ local.Exit(exitCode)
+ os.Exit(exitCode)
+}
+
+func setUpFS() error {
+ const dirPerm = 0700
+ mkdirMount := func(mountPath string, durability idb.TransactionDurability) error {
+ if err := os.MkdirAll(mountPath, dirPerm); err != nil {
+ return err
+ }
+ if err := overlayIndexedDB(mountPath, durability); err != nil {
+ return err
+ }
+ return nil
+ }
+
+ if err := mkdirMount("/bin", idb.DurabilityRelaxed); err != nil {
+ return err
+ }
+ if err := mkdirMount("/home/me", idb.DurabilityDefault); err != nil {
+ return err
+ }
+ if err := mkdirMount("/home/me/.cache", idb.DurabilityRelaxed); err != nil {
+ return err
+ }
+ if err := mkdirMount("/tmp", idb.DurabilityRelaxed); err != nil {
+ return err
+ }
+ if err := mkdirMount("/usr/local/go", idb.DurabilityRelaxed); err != nil {
+ return err
+ }
+ return nil
+}
+
+func overlayIndexedDB(mountPath string, durability idb.TransactionDurability) error {
+ idbFS, err := indexeddb.NewFS(context.Background(), mountPath, indexeddb.Options{
+ TransactionDurability: durability,
+ })
+ if err != nil {
+ return err
+ }
+ return fs.Overlay(mountPath, idbFS)
+}
diff --git a/go.mod b/go.mod
index 57315e8..ad4e7e8 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.16
require (
github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1
github.com/hack-pad/go-indexeddb v0.1.0
- github.com/hack-pad/hackpadfs v0.1.2
+ github.com/hack-pad/hackpadfs v0.1.4
github.com/hack-pad/hush v0.1.0
github.com/johnstarich/go/datasize v0.0.1
github.com/machinebox/progress v0.2.0
diff --git a/go.sum b/go.sum
index 954e55c..851be9e 100644
--- a/go.sum
+++ b/go.sum
@@ -16,6 +16,8 @@ github.com/hack-pad/hackpadfs v0.1.1 h1:DhzS50ln5XAOxZ0Xlnb/o3P/+MWUqlcbGdOQ5m+F
github.com/hack-pad/hackpadfs v0.1.1/go.mod h1:8bsINHOQhQUioUUiCzCyZZNLfEXjs0RwBIf3lTG+CEg=
github.com/hack-pad/hackpadfs v0.1.2 h1:ZsHfvrNAMNNBVLMKprOiN2rLD37x+YGj3QPJrhUdRF4=
github.com/hack-pad/hackpadfs v0.1.2/go.mod h1:8bsINHOQhQUioUUiCzCyZZNLfEXjs0RwBIf3lTG+CEg=
+github.com/hack-pad/hackpadfs v0.1.4 h1:vwLyuaVPFDqiy6YjLzvQ5fBTt0upzCaCkTok9aoKOdY=
+github.com/hack-pad/hackpadfs v0.1.4/go.mod h1:8bsINHOQhQUioUUiCzCyZZNLfEXjs0RwBIf3lTG+CEg=
github.com/hack-pad/hush v0.0.0-20210730065049-bd589dbef3a3 h1:0WBvEONkD8zXBRe7+5+mp34L2Upmok0yPKvOqOzpksw=
github.com/hack-pad/hush v0.0.0-20210730065049-bd589dbef3a3/go.mod h1:NqjEIfyA2YtlnEPlI/1K3tNuyXGByWFadPxPlGrDPms=
github.com/hack-pad/hush v0.1.0 h1:lm/iUaRpVsKkpbN6U9wf45arVnCXzTqsMG1jyihIgkI=
diff --git a/http_get.go b/http_get.go
index d00aaa0..1004e24 100644
--- a/http_get.go
+++ b/http_get.go
@@ -1,3 +1,4 @@
+//go:build js
// +build js
package main
diff --git a/install.go b/install.go
index 3d61311..571d5f9 100644
--- a/install.go
+++ b/install.go
@@ -9,30 +9,22 @@ import (
"runtime"
"syscall/js"
- "github.com/hack-pad/hackpad/internal/interop"
"github.com/hack-pad/hackpad/internal/log"
- "github.com/hack-pad/hackpad/internal/process"
- "github.com/hack-pad/hackpad/internal/promise"
)
-func installFunc(this js.Value, args []js.Value) interface{} {
- resolve, reject, prom := promise.New()
- go func() {
- err := install(args)
- if err != nil {
- reject(interop.WrapAsJSError(err, "Failed to install binary"))
- return
- }
- resolve(nil)
- }()
- return prom
+func (s domShim) installFunc(this js.Value, args []js.Value) (js.Wrapper, error) {
+ return nil, s.install(args)
}
-func install(args []js.Value) error {
+func (s domShim) install(args []js.Value) error {
if len(args) != 1 {
return errors.New("Expected command name to install")
}
command := args[0].String()
+ return s.Install(command)
+}
+
+func (s domShim) Install(command string) error {
command = filepath.Base(command) // ensure no path chars are present
if err := os.MkdirAll("/bin", 0644); err != nil {
@@ -44,13 +36,12 @@ func install(args []js.Value) error {
return err
}
defer runtime.GC()
- fs := process.Current().Files()
- fd, err := fs.Open("/bin/"+command, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0750)
+ file, err := os.OpenFile("/bin/"+command, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0750)
if err != nil {
return err
}
- defer fs.Close(fd)
- if _, err := fs.Write(fd, body, 0, body.Len(), nil); err != nil {
+ defer file.Close()
+ if _, err := file.Write(body.Bytes()); err != nil {
return err
}
log.Print("Install completed: ", command)
diff --git a/internal/common/fid.go b/internal/common/fid.go
index e34468f..0d57972 100644
--- a/internal/common/fid.go
+++ b/internal/common/fid.go
@@ -2,6 +2,9 @@ package common
import (
"fmt"
+ "io"
+
+ "github.com/hack-pad/hackpadfs"
)
type FID uint64
@@ -12,3 +15,11 @@ func (f *FID) String() string {
}
return fmt.Sprintf("%d", *f)
}
+
+type OpenFileAttr struct {
+ FilePath string
+ SeekOffset int64
+ Flags int
+ Mode hackpadfs.FileMode
+ RawDevice io.ReadWriteCloser
+}
diff --git a/internal/fs/device_file.go b/internal/fs/device_file.go
new file mode 100644
index 0000000..ee95594
--- /dev/null
+++ b/internal/fs/device_file.go
@@ -0,0 +1,40 @@
+package fs
+
+import (
+ "io"
+
+ "github.com/hack-pad/hackpadfs"
+ "github.com/pkg/errors"
+)
+
+type deviceFile struct {
+ name string
+ rawDevice io.ReadWriteCloser
+}
+
+var _ hackpadfs.File = &deviceFile{}
+
+func newDeviceFile(name string, rawDevice io.ReadWriteCloser) *deviceFile {
+ return &deviceFile{
+ name: name,
+ rawDevice: rawDevice,
+ }
+}
+
+func (d *deviceFile) Read(p []byte) (n int, err error) {
+ n, err = d.rawDevice.Read(p)
+ return n, errors.WithStack(err)
+}
+
+func (d *deviceFile) Write(p []byte) (n int, err error) {
+ n, err = d.rawDevice.Write(p)
+ return n, errors.WithStack(err)
+}
+
+func (d *deviceFile) Close() error {
+ return d.rawDevice.Close()
+}
+
+func (d *deviceFile) Stat() (hackpadfs.FileInfo, error) {
+ return newNamedFileInfo(d.name), nil
+}
diff --git a/internal/fs/file_descriptors.go b/internal/fs/file_descriptors.go
index 3d5849c..68dbdd0 100644
--- a/internal/fs/file_descriptors.go
+++ b/internal/fs/file_descriptors.go
@@ -1,6 +1,7 @@
package fs
import (
+ "context"
"io"
"os"
"path"
@@ -13,12 +14,14 @@ import (
"github.com/hack-pad/hackpad/internal/common"
"github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jserror"
+ "github.com/hack-pad/hackpad/internal/log"
"github.com/hack-pad/hackpadfs"
"github.com/pkg/errors"
)
var (
- ErrNotDir = interop.NewError("not a directory", "ENOTDIR")
+ ErrNotDir = jserror.New("not a directory", "ENOTDIR")
)
type FileDescriptors struct {
@@ -27,17 +30,23 @@ type FileDescriptors struct {
files map[FID]*fileDescriptor
mu sync.Mutex
workingDirectory *workingDirectory
+ locks *fileLocker
}
func NewStdFileDescriptors(parentPID common.PID, workingDirectory string) (*FileDescriptors, error) {
+ locker, err := newFileLocker(context.Background())
+ if err != nil {
+ return nil, err
+ }
f := &FileDescriptors{
parentPID: parentPID,
previousFID: 0,
files: make(map[FID]*fileDescriptor),
workingDirectory: newWorkingDirectory(workingDirectory),
+ locks: locker,
}
// order matters
- _, err := f.Open("/dev/stdin", syscall.O_RDONLY, 0)
+ _, err = f.Open("/dev/stdin", syscall.O_RDONLY, 0)
if err != nil {
return nil, err
}
@@ -49,35 +58,74 @@ func NewStdFileDescriptors(parentPID common.PID, workingDirectory string) (*File
return f, err
}
-func NewFileDescriptors(parentPID common.PID, workingDirectory string, parentFiles *FileDescriptors, inheritFDs []Attr) (*FileDescriptors, func(wd string) error, error) {
+func NewFileDescriptors(parentPID common.PID, workingDirectory string, openFiles []common.OpenFileAttr) (_ *FileDescriptors, _ func(wd string) error, returnedErr error) {
+ locker, err := newFileLocker(context.Background())
+ if err != nil {
+ return nil, nil, err
+ }
f := &FileDescriptors{
parentPID: parentPID,
previousFID: 0,
files: make(map[FID]*fileDescriptor),
workingDirectory: newWorkingDirectory(workingDirectory),
+ locks: locker,
}
- if len(inheritFDs) == 0 {
- inheritFDs = []Attr{{FID: 0}, {FID: 1}, {FID: 2}}
+ type openFile struct {
+ attr common.OpenFileAttr
+ file hackpadfs.File
}
- if len(inheritFDs) < 3 {
- return nil, nil, errors.Errorf("Invalid number of inherited file descriptors, must be 0 or at least 3: %#v", inheritFDs)
- }
- for _, attr := range inheritFDs {
- var inheritFD FID
- switch {
- case attr.Ignore:
- return nil, nil, errors.New("Ignored file descriptors are unsupported") // TODO be sure to align FDs properly when skipping iterations
- case attr.Pipe:
- return nil, nil, errors.New("Pipe file descriptors are unsupported") // TODO align FDs like Ignore, but child FIDs on stdio property must be different than the real FIDs (see node docs)
- default:
- inheritFD = attr.FID
+ var files []openFile
+ defer func() {
+ if returnedErr != nil {
+ returnedErr = errors.WithStack(returnedErr)
+ for _, f := range files {
+ f.file.Close()
+ }
+ }
+ }()
+ switch {
+ case len(openFiles) == 0:
+ stdin, err := getFile("dev/stdin", 0, 0)
+ if err != nil {
+ return nil, nil, err
}
- parentFD := parentFiles.files[inheritFD]
- if parentFD == nil {
- return nil, nil, errors.Errorf("Invalid parent FID %d", attr.FID)
+ stdout, err := getFile("dev/stdout", 0, 0)
+ if err != nil {
+ return nil, nil, err
}
+ stderr, err := getFile("dev/stderr", 0, 0)
+ if err != nil {
+ return nil, nil, err
+ }
+ files = append(files,
+ openFile{common.OpenFileAttr{FilePath: "/dev/stdin"}, stdin},
+ openFile{common.OpenFileAttr{FilePath: "/dev/stdout"}, stdout},
+ openFile{common.OpenFileAttr{FilePath: "/dev/stderr"}, stderr},
+ )
+ case len(openFiles) < 3:
+ return nil, nil, errors.Errorf("Invalid number of inherited file descriptors, must be 0 or at least 3: %#v", openFiles)
+ default:
+ for _, attr := range openFiles {
+ if attr.RawDevice == nil {
+ file, err := getFile(attr.FilePath, attr.Flags, attr.Mode)
+ if err != nil {
+ return nil, nil, err
+ }
+ _, err = hackpadfs.SeekFile(file, attr.SeekOffset, io.SeekStart)
+ if err != nil {
+ return nil, nil, err
+ }
+ files = append(files, openFile{attr, file})
+ } else {
+ file := newDeviceFile("", attr.RawDevice)
+ files = append(files, openFile{attr, file})
+ }
+ }
+ }
+
+ for _, file := range files {
fid := f.newFID()
- fd := parentFD.Dup(fid)
+ fd := newIrregularFileDescriptor(fid, path.Base(file.attr.FilePath), file.file, file.attr.Mode)
f.addFileDescriptor(fd)
fd.Open(parentPID)
}
@@ -139,6 +187,7 @@ func getFile(absPath string, flags int, mode os.FileMode) (hackpadfs.File, error
case "dev/stderr":
return stderr, nil
}
+ log.Debugf("Opening: %q %v %v", absPath, flags, mode)
return hackpadfs.OpenFile(filesystem, absPath, flags, mode)
}
@@ -277,42 +326,29 @@ const (
Unlock
)
-var (
- processFileLocks = make(map[string]*sync.RWMutex)
- newFileLockMu sync.Mutex
-)
-
func (f *FileDescriptors) Flock(fd FID, action LockAction) error {
fileDescriptor := f.files[fd]
if fileDescriptor == nil {
return interop.BadFileNumber(fd)
}
absPath := fileDescriptor.FileName()
- if _, ok := processFileLocks[absPath]; !ok {
- newFileLockMu.Lock()
- if _, ok := processFileLocks[absPath]; !ok {
- processFileLocks[absPath] = new(sync.RWMutex)
- }
- newFileLockMu.Unlock()
- }
- lock := processFileLocks[absPath]
switch action {
case LockShared, LockExclusive:
- // TODO support shared locks
- lock.Lock()
+ return f.locks.Lock(context.Background(), absPath, action == LockShared)
case Unlock:
- lock.Unlock()
+ return f.locks.Unlock(context.Background(), absPath)
default:
return interop.ErrNotImplemented
}
- return nil
}
-func (f *FileDescriptors) RawFID(fid FID) (io.Reader, error) {
- if _, ok := f.files[fid]; !ok {
+func (f *FileDescriptors) OpenRawFID(pid common.PID, fid FID) (hackpadfs.File, error) {
+ fd, ok := f.files[fid]
+ if !ok {
return nil, interop.BadFileNumber(fid)
}
- return f.files[fid].file, nil
+ fd.Open(pid)
+ return fd.file, nil
}
func (f *FileDescriptors) RawFIDs() []io.Reader {
diff --git a/internal/fs/file_locker.go b/internal/fs/file_locker.go
new file mode 100644
index 0000000..2496ebc
--- /dev/null
+++ b/internal/fs/file_locker.go
@@ -0,0 +1,209 @@
+package fs
+
+import (
+ "context"
+ "errors"
+ "syscall/js"
+ "time"
+
+ "github.com/hack-pad/go-indexeddb/idb"
+)
+
+type fileLocker struct {
+ db *idb.Database
+}
+
+const (
+ locksObjectStore = "locks"
+
+ sharedCountField = "sharedCount"
+)
+
+func newFileLocker(ctx context.Context) (*fileLocker, error) {
+ openRequest, err := idb.Global().Open(ctx, "file-locks", 1, func(db *idb.Database, oldVersion, newVersion uint) error {
+ _, err := db.CreateObjectStore(locksObjectStore, idb.ObjectStoreOptions{})
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+ db, err := openRequest.Await(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &fileLocker{
+ db: db,
+ }, nil
+}
+
+func (f *fileLocker) Lock(ctx context.Context, filePath string, shared bool) error {
+ locked, err := f.tryLock(ctx, filePath, shared)
+ if locked || err != nil {
+ return err
+ }
+
+ // lock is either exclusive or does not match the current lock type. must wait its turn.
+ const pollInterval = 10 * time.Millisecond
+ ticker := time.NewTicker(pollInterval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-ticker.C:
+ drainTicker(ticker)
+ locked, err := f.tryLock(ctx, filePath, shared)
+ if locked || err != nil {
+ return err
+ }
+ }
+ }
+}
+
+func (f *fileLocker) tryLock(ctx context.Context, filePath string, shared bool) (locked bool, err error) {
+ txn, err := f.db.Transaction(idb.TransactionReadWrite, locksObjectStore)
+ if err != nil {
+ return false, err
+ }
+ locks, err := txn.ObjectStore(locksObjectStore)
+ if err != nil {
+ return false, err
+ }
+ jsKey := js.ValueOf(filePath)
+ req, err := locks.Get(jsKey)
+ if err != nil {
+ return false, err
+ }
+ tryLock := func() (locked bool, err error) {
+ lock, err := req.Result()
+ if err != nil {
+ return false, err
+ }
+ if !lock.Truthy() {
+ // lock not yet held
+ sharedCount := 0
+ if shared {
+ sharedCount++
+ }
+ err := putLock(locks, jsKey, sharedCount)
+ if err != nil {
+ return false, err
+ }
+ return true, txn.Commit()
+ }
+
+ // lock is held
+ sharedCount, err := getSharedCount(lock)
+ if err != nil {
+ return false, err
+ }
+ isShared := sharedCount > 0
+ if shared {
+ sharedCount++
+ }
+ if shared && isShared { // lock already held by shared: add 1 and return
+ err := putLock(locks, jsKey, sharedCount+1)
+ if err != nil {
+ return false, err
+ }
+ return true, txn.Commit()
+ }
+ return false, nil
+ }
+ var listenErr error
+ req.ListenSuccess(ctx, func() {
+ locked, listenErr = tryLock()
+ if listenErr != nil {
+ txn.Abort()
+ return
+ }
+ })
+ err = txn.Await(ctx)
+ if listenErr != nil {
+ return false, listenErr
+ }
+ if err != nil {
+ return false, err
+ }
+ return locked, nil
+}
+
+func putLock(locks *idb.ObjectStore, key js.Value, sharedCount int) error {
+ _, err := locks.PutKey(key, js.ValueOf(map[string]interface{}{
+ sharedCountField: sharedCount,
+ }))
+ return err
+}
+
+func getSharedCount(lock js.Value) (int, error) {
+ jsSharedCount := lock.Get(sharedCountField)
+ if jsSharedCount.Type() != js.TypeNumber {
+ return 0, errors.New("malformed shared count")
+ }
+ return jsSharedCount.Int(), nil
+}
+
+func drainTicker(ticker *time.Ticker) {
+ for {
+ select {
+ case _, ok := <-ticker.C:
+ if !ok {
+ return
+ }
+ default:
+ return
+ }
+ }
+}
+
+func (f *fileLocker) Unlock(ctx context.Context, filePath string) error {
+ txn, err := f.db.Transaction(idb.TransactionReadWrite, locksObjectStore)
+ if err != nil {
+ return err
+ }
+ locks, err := txn.ObjectStore(locksObjectStore)
+ if err != nil {
+ return err
+ }
+ jsKey := js.ValueOf(filePath)
+ req, err := locks.Get(jsKey)
+ if err != nil {
+ return err
+ }
+ tryUnlock := func() error {
+ lock, err := req.Result()
+ if err != nil {
+ return err
+ }
+ sharedCount, err := getSharedCount(lock)
+ if err != nil {
+ return err
+ }
+ if sharedCount <= 1 { // is exclusive lock or last shared lock
+ _, err := locks.Delete(jsKey)
+ if err != nil {
+ return err
+ }
+ return txn.Commit()
+ }
+ sharedCount--
+ err = putLock(locks, jsKey, sharedCount)
+ if err != nil {
+ return err
+ }
+ return txn.Commit()
+ }
+ var listenErr error
+ req.ListenSuccess(ctx, func() {
+ listenErr = tryUnlock()
+ if listenErr != nil {
+ txn.Abort()
+ return
+ }
+ })
+ err = txn.Await(ctx)
+ if listenErr != nil {
+ return listenErr
+ }
+ return err
+}
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
index 2decb1c..6a5665e 100644
--- a/internal/fs/fs.go
+++ b/internal/fs/fs.go
@@ -61,6 +61,7 @@ func OverlayTarGzip(mountPath string, gzipReader io.ReadCloser, persist bool, sh
return err
}
+ originalMountPath := mountPath
mountPath = common.ResolvePath(".", mountPath)
if !persist {
underlyingFS, err := mem.NewFS()
@@ -78,7 +79,7 @@ func OverlayTarGzip(mountPath string, gzipReader io.ReadCloser, persist bool, sh
const tarfsDoneMarker = ".tarfs-complete"
- underlyingFS, err := newPersistDB(mountPath, true, shouldCache)
+ underlyingFS, err := newPersistDB(originalMountPath, true, shouldCache)
if err != nil {
return err
}
diff --git a/internal/fs/null_file.go b/internal/fs/null_file.go
index bab7f53..884275c 100644
--- a/internal/fs/null_file.go
+++ b/internal/fs/null_file.go
@@ -3,7 +3,6 @@ package fs
import (
"io"
"os"
- "time"
"github.com/hack-pad/hackpadfs"
)
@@ -22,16 +21,5 @@ func (f nullFile) ReadAt(p []byte, off int64) (n int, err error) { return 0, io
func (f nullFile) Seek(offset int64, whence int) (int64, error) { return 0, nil }
func (f nullFile) Write(p []byte) (n int, err error) { return len(p), nil }
func (f nullFile) WriteAt(p []byte, off int64) (n int, err error) { return len(p), nil }
-func (f nullFile) Stat() (os.FileInfo, error) { return nullStat{f}, nil }
+func (f nullFile) Stat() (os.FileInfo, error) { return namedFileInfo{f.name}, nil }
func (f nullFile) Truncate(size int64) error { return nil }
-
-type nullStat struct {
- f nullFile
-}
-
-func (s nullStat) Name() string { return s.f.name }
-func (s nullStat) Size() int64 { return 0 }
-func (s nullStat) Mode() os.FileMode { return 0 }
-func (s nullStat) ModTime() time.Time { return time.Time{} }
-func (s nullStat) IsDir() bool { return false }
-func (s nullStat) Sys() interface{} { return nil }
diff --git a/internal/fs/pipe.go b/internal/fs/pipe.go
index e4ddb39..c10aa27 100644
--- a/internal/fs/pipe.go
+++ b/internal/fs/pipe.go
@@ -88,16 +88,30 @@ func (p *pipeChan) Sync() error {
}
func (p *pipeChan) Read(buf []byte) (n int, err error) {
+ // Read should always block if the pipe is not closed
+ b, ok := <-p.buf
+ if !ok {
+ err = io.EOF
+ return
+ }
+ buf[n] = b
+ n++
+
for n < len(buf) {
- // Read should always block if the pipe is not closed
- b, ok := <-p.buf
- if !ok {
- err = io.EOF
- return
+ // attempt to read anything else if we still have room
+ select {
+ case b, ok := <-p.buf:
+ if !ok {
+ err = io.EOF
+ return
+ }
+ buf[n] = b
+ n++
+ default:
+ goto doneReading
}
- buf[n] = b
- n++
}
+doneReading:
if n == 0 {
err = io.EOF
}
diff --git a/internal/fs/stdout.go b/internal/fs/stdout.go
index 41a0595..b6f3682 100644
--- a/internal/fs/stdout.go
+++ b/internal/fs/stdout.go
@@ -79,3 +79,22 @@ func (b *bufferedLogger) Close() error {
// TODO prevent writes and return os.ErrClosed
return nil
}
+
+func (b *bufferedLogger) Stat() (hackpadfs.FileInfo, error) {
+ return namedFileInfo{b.name}, nil
+}
+
+type namedFileInfo struct {
+ name string
+}
+
+func newNamedFileInfo(name string) hackpadfs.FileInfo {
+ return namedFileInfo{name: name}
+}
+
+func (i namedFileInfo) Name() string { return i.name }
+func (i namedFileInfo) Size() int64 { return 0 }
+func (i namedFileInfo) Mode() hackpadfs.FileMode { return 0 }
+func (i namedFileInfo) ModTime() time.Time { return time.Time{} }
+func (i namedFileInfo) IsDir() bool { return false }
+func (i namedFileInfo) Sys() interface{} { return nil }
diff --git a/internal/interop/error.go b/internal/interop/error.go
index 6a22570..aeae714 100644
--- a/internal/interop/error.go
+++ b/internal/interop/error.go
@@ -2,82 +2,19 @@ package interop
import (
"fmt"
- "io"
- "os/exec"
"github.com/hack-pad/hackpad/internal/common"
- "github.com/hack-pad/hackpad/internal/log"
- "github.com/hack-pad/hackpadfs"
- "github.com/pkg/errors"
+ "github.com/hack-pad/hackpad/internal/jserror"
)
var (
- ErrNotImplemented = NewError("operation not supported", "ENOSYS")
+ ErrNotImplemented = jserror.New("operation not supported", "ENOSYS")
)
-type Error interface {
- error
- Message() string
- Code() string
-}
-
-type interopErr struct {
- error
- code string
-}
-
-func NewError(message, code string) Error {
- return WrapErr(errors.New(message), code)
-}
-
-func WrapErr(err error, code string) Error {
- return &interopErr{
- error: err,
- code: code,
- }
-}
-
-func (e *interopErr) Message() string {
- return e.Error()
-}
-
-func (e *interopErr) Code() string {
- return e.code
-}
-
-// errno names pulled from syscall/tables_js.go
-func mapToErrNo(err error, debugMessage string) string {
- if err, ok := err.(Error); ok {
- return err.Code()
- }
- if err, ok := err.(interface{ Unwrap() error }); ok {
- return mapToErrNo(err.Unwrap(), debugMessage)
- }
- switch err {
- case io.EOF, exec.ErrNotFound:
- return "ENOENT"
- }
- switch {
- case errors.Is(err, hackpadfs.ErrClosed):
- return "EBADF" // if it was already closed, then the file descriptor was invalid
- case errors.Is(err, hackpadfs.ErrNotExist):
- return "ENOENT"
- case errors.Is(err, hackpadfs.ErrExist):
- return "EEXIST"
- case errors.Is(err, hackpadfs.ErrIsDir):
- return "EISDIR"
- case errors.Is(err, hackpadfs.ErrPermission):
- return "EPERM"
- default:
- log.Errorf("Unknown error type: (%T) %+v\n\n%s", err, err, debugMessage)
- return "EPERM"
- }
-}
-
func BadFileNumber(fd common.FID) error {
- return NewError(fmt.Sprintf("Bad file number %d", fd), "EBADF")
+ return jserror.New(fmt.Sprintf("Bad file number %d", fd), "EBADF")
}
func BadFileErr(identifier string) error {
- return NewError(fmt.Sprintf("Bad file %q", identifier), "EBADF")
+ return jserror.New(fmt.Sprintf("Bad file %q", identifier), "EBADF")
}
diff --git a/internal/interop/funcs.go b/internal/interop/funcs.go
index 4881c7a..e3e4ced 100644
--- a/internal/interop/funcs.go
+++ b/internal/interop/funcs.go
@@ -1,3 +1,4 @@
+//go:build js
// +build js
package interop
@@ -8,6 +9,7 @@ import (
"strings"
"syscall/js"
+ "github.com/hack-pad/hackpad/internal/jserror"
"github.com/hack-pad/hackpad/internal/log"
"github.com/pkg/errors"
)
@@ -68,7 +70,7 @@ func setFuncHandler(name string, fn interface{}, args []js.Value) (returnedVal i
}()
ret, err = fn(args)
- err = wrapAsJSError(err, name, args...)
+ err = jserror.WrapArgs(err, name, args...)
ret = append([]interface{}{err}, ret...)
callback.Invoke(ret...)
}()
diff --git a/internal/interop/profile.go b/internal/interop/profile.go
index 3b488ff..b880472 100644
--- a/internal/interop/profile.go
+++ b/internal/interop/profile.go
@@ -1,3 +1,4 @@
+//go:build js
// +build js
package interop
@@ -15,12 +16,10 @@ import (
)
func ProfileJS(this js.Value, args []js.Value) interface{} {
- go func() {
- MemoryProfileJS(this, args)
- // Re-enable once these profiles actually work in the browser. Currently produces 0 samples.
- //TraceProfileJS(this, args)
- //StartCPUProfileJS(this, args)
- }()
+ MemoryProfileJS(this, args)
+ // Re-enable once these profiles actually work in the browser. Currently produces 0 samples.
+ //TraceProfileJS(this, args)
+ //StartCPUProfileJS(this, args)
return nil
}
diff --git a/internal/interop/values.go b/internal/interop/values.go
index eaf09af..2de0d8a 100644
--- a/internal/interop/values.go
+++ b/internal/interop/values.go
@@ -65,3 +65,11 @@ func StringMap(m map[string]string) js.Value {
}
return js.ValueOf(jsValue)
}
+
+func StringMapFromJSObject(v js.Value) map[string]string {
+ m := make(map[string]string)
+ for key, value := range Entries(v) {
+ m[key] = value.String()
+ }
+ return m
+}
diff --git a/internal/js/fs/chmod.go b/internal/js/fs/chmod.go
index 6977ba5..805d7c7 100644
--- a/internal/js/fs/chmod.go
+++ b/internal/js/fs/chmod.go
@@ -6,22 +6,20 @@ import (
"os"
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func chmod(args []js.Value) ([]interface{}, error) {
- _, err := chmodSync(args)
+func (s fileShim) chmod(args []js.Value) ([]interface{}, error) {
+ _, err := s.chmodSync(args)
return nil, err
}
-func chmodSync(args []js.Value) (interface{}, error) {
+func (s fileShim) chmodSync(args []js.Value) (interface{}, error) {
if len(args) != 2 {
return nil, errors.Errorf("Invalid number of args, expected 2: %v", args)
}
path := args[0].String()
mode := os.FileMode(args[1].Int())
- p := process.Current()
- return nil, p.Files().Chmod(path, mode)
+ return nil, s.process.Files().Chmod(path, mode)
}
diff --git a/internal/js/fs/chown.go b/internal/js/fs/chown.go
index 779671a..62539b4 100644
--- a/internal/js/fs/chown.go
+++ b/internal/js/fs/chown.go
@@ -8,12 +8,12 @@ import (
"github.com/pkg/errors"
)
-func chown(args []js.Value) ([]interface{}, error) {
- _, err := chownSync(args)
+func (s fileShim) chown(args []js.Value) ([]interface{}, error) {
+ _, err := s.chownSync(args)
return nil, err
}
-func chownSync(args []js.Value) (interface{}, error) {
+func (s fileShim) chownSync(args []js.Value) (interface{}, error) {
if len(args) != 3 {
return nil, errors.Errorf("Invalid number of args, expected 3: %v", args)
}
@@ -21,10 +21,10 @@ func chownSync(args []js.Value) (interface{}, error) {
path := args[0].String()
uid := args[1].Int()
gid := args[2].Int()
- return nil, Chown(path, uid, gid)
+ return nil, s.Chown(path, uid, gid)
}
-func Chown(path string, uid, gid int) error {
+func (s fileShim) Chown(path string, uid, gid int) error {
// TODO no-op, consider adding user and group ID support to hackpadfs
return nil
}
diff --git a/internal/js/fs/close.go b/internal/js/fs/close.go
index c1c8919..41c9b67 100644
--- a/internal/js/fs/close.go
+++ b/internal/js/fs/close.go
@@ -6,22 +6,20 @@ import (
"syscall/js"
"github.com/hack-pad/hackpad/internal/fs"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func closeFn(args []js.Value) ([]interface{}, error) {
- ret, err := closeSync(args)
+func (s fileShim) closeFn(args []js.Value) ([]interface{}, error) {
+ ret, err := s.closeSync(args)
return []interface{}{ret}, err
}
-func closeSync(args []js.Value) (interface{}, error) {
+func (s fileShim) closeSync(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.Errorf("not enough args %d", len(args))
}
fd := fs.FID(args[0].Int())
- p := process.Current()
- err := p.Files().Close(fd)
+ err := s.process.Files().Close(fd)
return nil, err
}
diff --git a/internal/js/fs/fchmod.go b/internal/js/fs/fchmod.go
index 8c04fa5..32e9589 100644
--- a/internal/js/fs/fchmod.go
+++ b/internal/js/fs/fchmod.go
@@ -7,22 +7,20 @@ import (
"syscall/js"
"github.com/hack-pad/hackpad/internal/common"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func fchmod(args []js.Value) ([]interface{}, error) {
- _, err := fchmodSync(args)
+func (s fileShim) fchmod(args []js.Value) ([]interface{}, error) {
+ _, err := s.fchmodSync(args)
return nil, err
}
-func fchmodSync(args []js.Value) (interface{}, error) {
+func (s fileShim) fchmodSync(args []js.Value) (interface{}, error) {
if len(args) != 2 {
return nil, errors.Errorf("Invalid number of args, expected 2: %v", args)
}
fid := common.FID(args[0].Int())
mode := os.FileMode(args[1].Int())
- p := process.Current()
- return nil, p.Files().Fchmod(fid, mode)
+ return nil, s.process.Files().Fchmod(fid, mode)
}
diff --git a/internal/js/fs/flock.go b/internal/js/fs/flock.go
index cf9f041..7e68b1f 100644
--- a/internal/js/fs/flock.go
+++ b/internal/js/fs/flock.go
@@ -8,16 +8,15 @@ import (
"github.com/hack-pad/hackpad/internal/common"
"github.com/hack-pad/hackpad/internal/fs"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func flock(args []js.Value) ([]interface{}, error) {
- _, err := flockSync(args)
+func (s fileShim) flock(args []js.Value) ([]interface{}, error) {
+ _, err := s.flockSync(args)
return nil, err
}
-func flockSync(args []js.Value) (interface{}, error) {
+func (s fileShim) flockSync(args []js.Value) (interface{}, error) {
if len(args) != 2 {
return nil, errors.Errorf("Invalid number of args, expected 2: %v", args)
}
@@ -34,10 +33,9 @@ func flockSync(args []js.Value) (interface{}, error) {
action = fs.Unlock
}
- return nil, Flock(fid, action, shouldLock)
+ return nil, s.Flock(fid, action, shouldLock)
}
-func Flock(fid common.FID, action fs.LockAction, shouldLock bool) error {
- p := process.Current()
- return p.Files().Flock(fid, action)
+func (s fileShim) Flock(fid common.FID, action fs.LockAction, shouldLock bool) error {
+ return s.process.Files().Flock(fid, action)
}
diff --git a/internal/js/fs/fs.go b/internal/js/fs/fs.go
index 3d84e68..331d384 100644
--- a/internal/js/fs/fs.go
+++ b/internal/js/fs/fs.go
@@ -12,20 +12,18 @@ import (
"github.com/hack-pad/hackpad/internal/fs"
"github.com/hack-pad/hackpad/internal/global"
"github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jserror"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
"github.com/hack-pad/hackpad/internal/process"
- "github.com/hack-pad/hackpad/internal/promise"
)
-/*
-fchown(fd, uid, gid, callback) { callback(enosys()); },
-lchown(path, uid, gid, callback) { callback(enosys()); },
-link(path, link, callback) { callback(enosys()); },
-readlink(path, callback) { callback(enosys()); },
-symlink(path, link, callback) { callback(enosys()); },
-truncate(path, length, callback) { callback(enosys()); },
-*/
+type fileShim struct {
+ process *process.Process
+}
+
+func Init(process *process.Process) {
+ shim := fileShim{process}
-func Init() {
fs := js.Global().Get("fs")
constants := fs.Get("constants")
constants.Set("O_RDONLY", syscall.O_RDONLY)
@@ -35,95 +33,86 @@ func Init() {
constants.Set("O_TRUNC", syscall.O_TRUNC)
constants.Set("O_APPEND", syscall.O_APPEND)
constants.Set("O_EXCL", syscall.O_EXCL)
- interop.SetFunc(fs, "chmod", chmod)
- interop.SetFunc(fs, "chmodSync", chmodSync)
- interop.SetFunc(fs, "chown", chown)
- interop.SetFunc(fs, "chownSync", chownSync)
- interop.SetFunc(fs, "close", closeFn)
- interop.SetFunc(fs, "closeSync", closeSync)
- interop.SetFunc(fs, "fchmod", fchmod)
- interop.SetFunc(fs, "fchmodSync", fchmodSync)
- interop.SetFunc(fs, "flock", flock)
- interop.SetFunc(fs, "flockSync", flockSync)
- interop.SetFunc(fs, "fstat", fstat)
- interop.SetFunc(fs, "fstatSync", fstatSync)
- interop.SetFunc(fs, "fsync", fsync)
- interop.SetFunc(fs, "fsyncSync", fsyncSync)
- interop.SetFunc(fs, "ftruncate", ftruncate)
- interop.SetFunc(fs, "ftruncateSync", ftruncateSync)
- interop.SetFunc(fs, "lstat", lstat)
- interop.SetFunc(fs, "lstatSync", lstatSync)
- interop.SetFunc(fs, "mkdir", mkdir)
- interop.SetFunc(fs, "mkdirSync", mkdirSync)
- interop.SetFunc(fs, "open", open)
- interop.SetFunc(fs, "openSync", openSync)
- interop.SetFunc(fs, "pipe", pipe)
- interop.SetFunc(fs, "pipeSync", pipeSync)
- interop.SetFunc(fs, "read", read)
- interop.SetFunc(fs, "readSync", readSync)
- interop.SetFunc(fs, "readdir", readdir)
- interop.SetFunc(fs, "readdirSync", readdirSync)
- interop.SetFunc(fs, "rename", rename)
- interop.SetFunc(fs, "renameSync", renameSync)
- interop.SetFunc(fs, "rmdir", rmdir)
- interop.SetFunc(fs, "rmdirSync", rmdirSync)
- interop.SetFunc(fs, "stat", stat)
- interop.SetFunc(fs, "statSync", statSync)
- interop.SetFunc(fs, "unlink", unlink)
- interop.SetFunc(fs, "unlinkSync", unlinkSync)
- interop.SetFunc(fs, "utimes", utimes)
- interop.SetFunc(fs, "utimesSync", utimesSync)
- interop.SetFunc(fs, "write", write)
- interop.SetFunc(fs, "writeSync", writeSync)
+ interop.SetFunc(fs, "chmod", shim.chmod)
+ interop.SetFunc(fs, "chmodSync", shim.chmodSync)
+ interop.SetFunc(fs, "chown", shim.chown)
+ interop.SetFunc(fs, "chownSync", shim.chownSync)
+ interop.SetFunc(fs, "close", shim.closeFn)
+ interop.SetFunc(fs, "closeSync", shim.closeSync)
+ interop.SetFunc(fs, "fchmod", shim.fchmod)
+ interop.SetFunc(fs, "fchmodSync", shim.fchmodSync)
+ interop.SetFunc(fs, "flock", shim.flock)
+ interop.SetFunc(fs, "flockSync", shim.flockSync)
+ interop.SetFunc(fs, "fstat", shim.fstat)
+ interop.SetFunc(fs, "fstatSync", shim.fstatSync)
+ interop.SetFunc(fs, "fsync", shim.fsync)
+ interop.SetFunc(fs, "fsyncSync", shim.fsyncSync)
+ interop.SetFunc(fs, "ftruncate", shim.ftruncate)
+ interop.SetFunc(fs, "ftruncateSync", shim.ftruncateSync)
+ interop.SetFunc(fs, "lstat", shim.lstat)
+ interop.SetFunc(fs, "lstatSync", shim.lstatSync)
+ interop.SetFunc(fs, "mkdir", shim.mkdir)
+ interop.SetFunc(fs, "mkdirSync", shim.mkdirSync)
+ interop.SetFunc(fs, "open", shim.open)
+ interop.SetFunc(fs, "openSync", shim.openSync)
+ interop.SetFunc(fs, "pipe", shim.pipe)
+ interop.SetFunc(fs, "pipeSync", shim.pipeSync)
+ interop.SetFunc(fs, "read", shim.read)
+ interop.SetFunc(fs, "readSync", shim.readSync)
+ interop.SetFunc(fs, "readdir", shim.readdir)
+ interop.SetFunc(fs, "readdirSync", shim.readdirSync)
+ interop.SetFunc(fs, "rename", shim.rename)
+ interop.SetFunc(fs, "renameSync", shim.renameSync)
+ interop.SetFunc(fs, "rmdir", shim.rmdir)
+ interop.SetFunc(fs, "rmdirSync", shim.rmdirSync)
+ interop.SetFunc(fs, "stat", shim.stat)
+ interop.SetFunc(fs, "statSync", shim.statSync)
+ interop.SetFunc(fs, "unlink", shim.unlink)
+ interop.SetFunc(fs, "unlinkSync", shim.unlinkSync)
+ interop.SetFunc(fs, "utimes", shim.utimes)
+ interop.SetFunc(fs, "utimesSync", shim.utimesSync)
+ interop.SetFunc(fs, "write", shim.write)
+ interop.SetFunc(fs, "writeSync", shim.writeSync)
- global.Set("getMounts", js.FuncOf(getMounts))
- global.Set("destroyMount", js.FuncOf(destroyMount))
- global.Set("overlayTarGzip", js.FuncOf(overlayTarGzip))
- global.Set("overlayIndexedDB", js.FuncOf(overlayIndexedDB))
- global.Set("dumpZip", js.FuncOf(dumpZip))
+ global.Set("getMounts", jsfunc.Promise(shim.getMounts))
+ global.Set("destroyMount", jsfunc.Promise(shim.destroyMount))
+ global.Set("overlayTarGzip", jsfunc.Promise(shim.overlayTarGzip))
+ global.Set("overlayIndexedDB", jsfunc.Promise(shim.overlayIndexedDB))
+ global.Set("dumpZip", jsfunc.Promise(shim.dumpZip))
// Set up system directories
- files := process.Current().Files()
+ files := process.Files()
if err := files.MkdirAll(os.TempDir(), 0777); err != nil {
panic(err)
}
}
-func Dump(basePath string) interface{} {
- basePath = common.ResolvePath(process.Current().WorkingDirectory(), basePath)
+func (s fileShim) Dump(basePath string) interface{} {
+ basePath = common.ResolvePath(s.process.WorkingDirectory(), basePath)
return fs.Dump(basePath)
}
-func dumpZip(this js.Value, args []js.Value) interface{} {
+func (s fileShim) dumpZip(this js.Value, args []js.Value) (js.Wrapper, error) {
if len(args) != 1 {
- return interop.WrapAsJSError(errors.New("dumpZip: file path is required"), "EINVAL")
+ return nil, jserror.Wrap(errors.New("dumpZip: file path is required"), "EINVAL")
}
path := args[0].String()
- path = common.ResolvePath(process.Current().WorkingDirectory(), path)
- return interop.WrapAsJSError(fs.DumpZip(path), "dumpZip")
+ path = common.ResolvePath(s.process.WorkingDirectory(), path)
+ return nil, jserror.Wrap(fs.DumpZip(path), "dumpZip")
}
-func getMounts(this js.Value, args []js.Value) interface{} {
+func (s fileShim) getMounts(this js.Value, args []js.Value) (js.Wrapper, error) {
var mounts []string
for _, p := range fs.Mounts() {
mounts = append(mounts, p.Path)
}
- return interop.SliceFromStrings(mounts)
+ return interop.SliceFromStrings(mounts), nil
}
-func destroyMount(this js.Value, args []js.Value) interface{} {
+func (s fileShim) destroyMount(this js.Value, args []js.Value) (js.Wrapper, error) {
if len(args) < 1 {
- return interop.WrapAsJSError(errors.New("destroyMount: mount path is required"), "EINVAL")
+ return nil, jserror.Wrap(errors.New("destroyMount: mount path is required"), "EINVAL")
}
- resolve, reject, prom := promise.New()
mountPath := args[0].String()
- go func() {
- err := interop.WrapAsJSError(fs.DestroyMount(mountPath), "destroyMount")
- if err != nil {
- reject(err)
- } else {
- resolve(nil)
- }
- }()
- return prom
+ return nil, jserror.Wrap(fs.DestroyMount(mountPath), "destroyMount")
}
diff --git a/internal/js/fs/fstat.go b/internal/js/fs/fstat.go
index aed0af1..887d42e 100644
--- a/internal/js/fs/fstat.go
+++ b/internal/js/fs/fstat.go
@@ -6,21 +6,19 @@ import (
"syscall/js"
"github.com/hack-pad/hackpad/internal/fs"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func fstat(args []js.Value) ([]interface{}, error) {
- info, err := fstatSync(args)
+func (s fileShim) fstat(args []js.Value) ([]interface{}, error) {
+ info, err := s.fstatSync(args)
return []interface{}{info}, err
}
-func fstatSync(args []js.Value) (interface{}, error) {
+func (s fileShim) fstatSync(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.Errorf("Invalid number of args, expected 1: %v", args)
}
fd := fs.FID(args[0].Int())
- p := process.Current()
- info, err := p.Files().Fstat(fd)
+ info, err := s.process.Files().Fstat(fd)
return jsStat(info), err
}
diff --git a/internal/js/fs/fsync.go b/internal/js/fs/fsync.go
index a42fd37..05d7c9c 100644
--- a/internal/js/fs/fsync.go
+++ b/internal/js/fs/fsync.go
@@ -6,22 +6,20 @@ import (
"syscall/js"
"github.com/hack-pad/hackpad/internal/fs"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
// fsync(fd, callback) { callback(null); },
-func fsync(args []js.Value) ([]interface{}, error) {
- _, err := fsyncSync(args)
+func (s fileShim) fsync(args []js.Value) ([]interface{}, error) {
+ _, err := s.fsyncSync(args)
return nil, err
}
-func fsyncSync(args []js.Value) (interface{}, error) {
+func (s fileShim) fsyncSync(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.Errorf("Invalid number of args, expected 1: %v", args)
}
fd := fs.FID(args[0].Int())
- p := process.Current()
- return nil, p.Files().Fsync(fd)
+ return nil, s.process.Files().Fsync(fd)
}
diff --git a/internal/js/fs/ftruncate.go b/internal/js/fs/ftruncate.go
index cf8d478..547fdc4 100644
--- a/internal/js/fs/ftruncate.go
+++ b/internal/js/fs/ftruncate.go
@@ -6,16 +6,15 @@ import (
"syscall/js"
"github.com/hack-pad/hackpad/internal/fs"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func ftruncateSync(args []js.Value) (interface{}, error) {
- _, err := ftruncate(args)
+func (s fileShim) ftruncateSync(args []js.Value) (interface{}, error) {
+ _, err := s.ftruncate(args)
return nil, err
}
-func ftruncate(args []js.Value) ([]interface{}, error) {
+func (s fileShim) ftruncate(args []js.Value) ([]interface{}, error) {
// args: fd, len
if len(args) == 0 {
return nil, errors.Errorf("missing required args, expected fd: %+v", args)
@@ -26,6 +25,5 @@ func ftruncate(args []js.Value) ([]interface{}, error) {
length = args[1].Int()
}
- p := process.Current()
- return nil, p.Files().Truncate(fd, int64(length))
+ return nil, s.process.Files().Truncate(fd, int64(length))
}
diff --git a/internal/js/fs/lstat.go b/internal/js/fs/lstat.go
index 402c65a..b54798f 100644
--- a/internal/js/fs/lstat.go
+++ b/internal/js/fs/lstat.go
@@ -5,21 +5,19 @@ package fs
import (
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func lstat(args []js.Value) ([]interface{}, error) {
- info, err := lstatSync(args)
+func (s fileShim) lstat(args []js.Value) ([]interface{}, error) {
+ info, err := s.lstatSync(args)
return []interface{}{info}, err
}
-func lstatSync(args []js.Value) (interface{}, error) {
+func (s fileShim) lstatSync(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.Errorf("Invalid number of args, expected 1: %v", args)
}
path := args[0].String()
- p := process.Current()
- info, err := p.Files().Lstat(path)
+ info, err := s.process.Files().Lstat(path)
return jsStat(info), err
}
diff --git a/internal/js/fs/mkdir.go b/internal/js/fs/mkdir.go
index 34a9cde..c9ffc9a 100644
--- a/internal/js/fs/mkdir.go
+++ b/internal/js/fs/mkdir.go
@@ -6,16 +6,15 @@ import (
"os"
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func mkdir(args []js.Value) ([]interface{}, error) {
- _, err := mkdirSync(args)
+func (s fileShim) mkdir(args []js.Value) ([]interface{}, error) {
+ _, err := s.mkdirSync(args)
return nil, err
}
-func mkdirSync(args []js.Value) (interface{}, error) {
+func (s fileShim) mkdirSync(args []js.Value) (interface{}, error) {
if len(args) != 2 {
return nil, errors.Errorf("Invalid number of args, expected 2: %v", args)
}
@@ -35,9 +34,8 @@ func mkdirSync(args []js.Value) (interface{}, error) {
recursive = true
}
- p := process.Current()
if recursive {
- return nil, p.Files().MkdirAll(path, mode)
+ return nil, s.process.Files().MkdirAll(path, mode)
}
- return nil, p.Files().Mkdir(path, mode)
+ return nil, s.process.Files().Mkdir(path, mode)
}
diff --git a/internal/js/fs/open.go b/internal/js/fs/open.go
index 6d48057..6ba381d 100644
--- a/internal/js/fs/open.go
+++ b/internal/js/fs/open.go
@@ -6,16 +6,15 @@ import (
"os"
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func open(args []js.Value) ([]interface{}, error) {
- fd, err := openSync(args)
+func (s fileShim) open(args []js.Value) ([]interface{}, error) {
+ fd, err := s.openSync(args)
return []interface{}{fd}, err
}
-func openSync(args []js.Value) (interface{}, error) {
+func (s fileShim) openSync(args []js.Value) (interface{}, error) {
if len(args) == 0 {
return nil, errors.Errorf("Expected path, received: %v", args)
}
@@ -29,7 +28,6 @@ func openSync(args []js.Value) (interface{}, error) {
mode = os.FileMode(args[2].Int())
}
- p := process.Current()
- fd, err := p.Files().Open(path, flags, mode)
+ fd, err := s.process.Files().Open(path, flags, mode)
return fd, err
}
diff --git a/internal/js/fs/overlay.go b/internal/js/fs/overlay.go
index d7ab43b..3e6633c 100644
--- a/internal/js/fs/overlay.go
+++ b/internal/js/fs/overlay.go
@@ -20,27 +20,21 @@ import (
"github.com/hack-pad/hackpad/internal/common"
"github.com/hack-pad/hackpad/internal/fs"
"github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jserror"
"github.com/hack-pad/hackpad/internal/log"
- "github.com/hack-pad/hackpad/internal/process"
- "github.com/hack-pad/hackpad/internal/promise"
"github.com/johnstarich/go/datasize"
)
-func overlayIndexedDB(this js.Value, args []js.Value) interface{} {
- resolve, reject, prom := promise.New()
- go func() {
- err := OverlayIndexedDB(args)
- if err != nil {
- reject(interop.WrapAsJSError(err, "Failed overlaying IndexedDB FS"))
- } else {
- log.Debug("Successfully overlayed IndexedDB FS")
- resolve(nil)
- }
- }()
- return prom
+func (s fileShim) overlayIndexedDB(this js.Value, args []js.Value) (js.Wrapper, error) {
+ err := s.OverlayIndexedDB(args)
+ if err != nil {
+ return nil, jserror.Wrap(err, "Failed overlaying IndexedDB FS")
+ }
+ log.Debug("Successfully overlayed IndexedDB FS")
+ return nil, nil
}
-func OverlayIndexedDB(args []js.Value) (err error) {
+func (s fileShim) OverlayIndexedDB(args []js.Value) (err error) {
if len(args) == 0 {
return errors.New("overlayIndexedDB: mount path is required")
}
@@ -64,22 +58,16 @@ func OverlayIndexedDB(args []js.Value) (err error) {
return fs.Overlay(mountPath, idbFS)
}
-func overlayTarGzip(this js.Value, args []js.Value) interface{} {
- resolve, reject, prom := promise.New()
- log.Debug("Backgrounding overlay request")
- go func() {
- err := OverlayTarGzip(args)
- if err != nil {
- reject(interop.WrapAsJSError(err, "Failed overlaying .tar.gz FS"))
- } else {
- log.Debug("Successfully overlayed .tar.gz FS")
- resolve(nil)
- }
- }()
- return prom
+func (s fileShim) overlayTarGzip(this js.Value, args []js.Value) (js.Wrapper, error) {
+ err := s.OverlayTarGzip(args)
+ if err != nil {
+ return nil, jserror.Wrap(err, "Failed overlaying .tar.gz FS")
+ }
+ log.Debug("Successfully overlayed .tar.gz FS")
+ return nil, nil
}
-func OverlayTarGzip(args []js.Value) error {
+func (s fileShim) OverlayTarGzip(args []js.Value) error {
if len(args) < 2 {
return errors.New("overlayTarGzip: mount path and .tar.gz URL path is required")
}
@@ -113,7 +101,7 @@ func OverlayTarGzip(args []js.Value) error {
if options["skipCacheDirs"].Type() == js.TypeObject {
skipDirs := make(map[string]bool)
for _, d := range interop.StringsFromJSValue(options["skipCacheDirs"]) {
- skipDirs[common.ResolvePath(process.Current().WorkingDirectory(), d)] = true
+ skipDirs[common.ResolvePath(s.process.WorkingDirectory(), d)] = true
}
maxFileBytes := datasize.Kibibytes(100).Bytes()
shouldCache = func(name string, info hackpadfs.FileInfo) bool {
diff --git a/internal/js/fs/pipe.go b/internal/js/fs/pipe.go
index 52808fc..bdcb7bd 100644
--- a/internal/js/fs/pipe.go
+++ b/internal/js/fs/pipe.go
@@ -5,20 +5,18 @@ package fs
import (
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func pipe(args []js.Value) ([]interface{}, error) {
- fds, err := pipeSync(args)
+func (s fileShim) pipe(args []js.Value) ([]interface{}, error) {
+ fds, err := s.pipeSync(args)
return []interface{}{fds}, err
}
-func pipeSync(args []js.Value) (interface{}, error) {
+func (s fileShim) pipeSync(args []js.Value) (interface{}, error) {
if len(args) != 0 {
return nil, errors.Errorf("Invalid number of args, expected 0: %v", args)
}
- p := process.Current()
- fds := p.Files().Pipe()
+ fds := s.process.Files().Pipe()
return []interface{}{fds[0], fds[1]}, nil
}
diff --git a/internal/js/fs/read.go b/internal/js/fs/read.go
index ce69947..1f3f38d 100644
--- a/internal/js/fs/read.go
+++ b/internal/js/fs/read.go
@@ -6,22 +6,21 @@ import (
"syscall/js"
"github.com/hack-pad/hackpad/internal/fs"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/hack-pad/hackpadfs/indexeddb/idbblob"
"github.com/pkg/errors"
)
-func read(args []js.Value) ([]interface{}, error) {
- n, buf, err := readSyncImpl(args)
+func (s fileShim) read(args []js.Value) ([]interface{}, error) {
+ n, buf, err := s.readSyncImpl(args)
return []interface{}{n, buf}, err
}
-func readSync(args []js.Value) (interface{}, error) {
- n, _, err := readSyncImpl(args)
+func (s fileShim) readSync(args []js.Value) (interface{}, error) {
+ n, _, err := s.readSyncImpl(args)
return n, err
}
-func readSyncImpl(args []js.Value) (int, js.Value, error) {
+func (s fileShim) readSyncImpl(args []js.Value) (int, js.Value, error) {
// args: fd, buffer, offset, length, position
if len(args) != 5 {
return 0, js.Null(), errors.Errorf("missing required args, expected 5: %+v", args)
@@ -39,7 +38,6 @@ func readSyncImpl(args []js.Value) (int, js.Value, error) {
*position = int64(args[4].Int())
}
- p := process.Current()
- n, err := p.Files().Read(fd, buffer, offset, length, position)
+ n, err := s.process.Files().Read(fd, buffer, offset, length, position)
return n, buffer.JSValue(), err
}
diff --git a/internal/js/fs/readdir.go b/internal/js/fs/readdir.go
index c06ca17..f24489a 100644
--- a/internal/js/fs/readdir.go
+++ b/internal/js/fs/readdir.go
@@ -5,22 +5,20 @@ package fs
import (
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func readdir(args []js.Value) ([]interface{}, error) {
- fileNames, err := readdirSync(args)
+func (s fileShim) readdir(args []js.Value) ([]interface{}, error) {
+ fileNames, err := s.readdirSync(args)
return []interface{}{fileNames}, err
}
-func readdirSync(args []js.Value) (interface{}, error) {
+func (s fileShim) readdirSync(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.Errorf("Invalid number of args, expected 1: %v", args)
}
path := args[0].String()
- p := process.Current()
- dir, err := p.Files().ReadDir(path)
+ dir, err := s.process.Files().ReadDir(path)
if err != nil {
return nil, err
}
diff --git a/internal/js/fs/rename.go b/internal/js/fs/rename.go
index 86731cf..c0481a3 100644
--- a/internal/js/fs/rename.go
+++ b/internal/js/fs/rename.go
@@ -5,23 +5,21 @@ package fs
import (
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
// rename(from, to, callback) { callback(enosys()); },
-func rename(args []js.Value) ([]interface{}, error) {
- _, err := renameSync(args)
+func (s fileShim) rename(args []js.Value) ([]interface{}, error) {
+ _, err := s.renameSync(args)
return nil, err
}
-func renameSync(args []js.Value) (interface{}, error) {
+func (s fileShim) renameSync(args []js.Value) (interface{}, error) {
if len(args) != 2 {
return nil, errors.Errorf("Invalid number of args, expected 2: %v", args)
}
oldPath := args[0].String()
newPath := args[1].String()
- p := process.Current()
- return nil, p.Files().Rename(oldPath, newPath)
+ return nil, s.process.Files().Rename(oldPath, newPath)
}
diff --git a/internal/js/fs/rmdir.go b/internal/js/fs/rmdir.go
index f15e5cf..80673eb 100644
--- a/internal/js/fs/rmdir.go
+++ b/internal/js/fs/rmdir.go
@@ -5,20 +5,18 @@ package fs
import (
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func rmdir(args []js.Value) ([]interface{}, error) {
- _, err := rmdirSync(args)
+func (s fileShim) rmdir(args []js.Value) ([]interface{}, error) {
+ _, err := s.rmdirSync(args)
return nil, err
}
-func rmdirSync(args []js.Value) (interface{}, error) {
+func (s fileShim) rmdirSync(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.Errorf("Invalid number of args, expected 1: %v", args)
}
path := args[0].String()
- p := process.Current()
- return nil, p.Files().RemoveDir(path)
+ return nil, s.process.Files().RemoveDir(path)
}
diff --git a/internal/js/fs/stat.go b/internal/js/fs/stat.go
index 1abdb85..96ae112 100644
--- a/internal/js/fs/stat.go
+++ b/internal/js/fs/stat.go
@@ -7,22 +7,20 @@ import (
"syscall"
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func stat(args []js.Value) ([]interface{}, error) {
- info, err := statSync(args)
+func (s fileShim) stat(args []js.Value) ([]interface{}, error) {
+ info, err := s.statSync(args)
return []interface{}{info}, err
}
-func statSync(args []js.Value) (interface{}, error) {
+func (s fileShim) statSync(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.Errorf("Invalid number of args, expected 1: %v", args)
}
path := args[0].String()
- p := process.Current()
- info, err := p.Files().Stat(path)
+ info, err := s.process.Files().Stat(path)
return jsStat(info), err
}
diff --git a/internal/js/fs/unlink.go b/internal/js/fs/unlink.go
index 4f396cc..86fb346 100644
--- a/internal/js/fs/unlink.go
+++ b/internal/js/fs/unlink.go
@@ -5,22 +5,20 @@ package fs
import (
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
// unlink(path, callback) { callback(enosys()); },
-func unlink(args []js.Value) ([]interface{}, error) {
- _, err := unlinkSync(args)
+func (s fileShim) unlink(args []js.Value) ([]interface{}, error) {
+ _, err := s.unlinkSync(args)
return nil, err
}
-func unlinkSync(args []js.Value) (interface{}, error) {
+func (s fileShim) unlinkSync(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.Errorf("Invalid number of args, expected 1: %v", args)
}
path := args[0].String()
- p := process.Current()
- return nil, p.Files().Unlink(path)
+ return nil, s.process.Files().Unlink(path)
}
diff --git a/internal/js/fs/utimes.go b/internal/js/fs/utimes.go
index 70765e7..cda71bb 100644
--- a/internal/js/fs/utimes.go
+++ b/internal/js/fs/utimes.go
@@ -6,16 +6,15 @@ import (
"syscall/js"
"time"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func utimes(args []js.Value) ([]interface{}, error) {
- _, err := utimesSync(args)
+func (s fileShim) utimes(args []js.Value) ([]interface{}, error) {
+ _, err := s.utimesSync(args)
return nil, err
}
-func utimesSync(args []js.Value) (interface{}, error) {
+func (s fileShim) utimesSync(args []js.Value) (interface{}, error) {
if len(args) != 3 {
return nil, errors.Errorf("Invalid number of args, expected 3: %v", args)
}
@@ -23,6 +22,5 @@ func utimesSync(args []js.Value) (interface{}, error) {
path := args[0].String()
atime := time.Unix(int64(args[1].Int()), 0)
mtime := time.Unix(int64(args[2].Int()), 0)
- p := process.Current()
- return nil, p.Files().Utimes(path, atime, mtime)
+ return nil, s.process.Files().Utimes(path, atime, mtime)
}
diff --git a/internal/js/fs/write.go b/internal/js/fs/write.go
index 053d981..3cde9f5 100644
--- a/internal/js/fs/write.go
+++ b/internal/js/fs/write.go
@@ -6,20 +6,19 @@ import (
"syscall/js"
"github.com/hack-pad/hackpad/internal/fs"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/hack-pad/hackpadfs/indexeddb/idbblob"
"github.com/pkg/errors"
)
-func writeSync(args []js.Value) (interface{}, error) {
- ret, err := write(args)
+func (s fileShim) writeSync(args []js.Value) (interface{}, error) {
+ ret, err := s.write(args)
if len(ret) > 1 {
return ret[0], err
}
return ret, err
}
-func write(args []js.Value) ([]interface{}, error) {
+func (s fileShim) write(args []js.Value) ([]interface{}, error) {
// args: fd, buffer, offset, length, position
if len(args) < 2 {
return nil, errors.Errorf("missing required args, expected fd and buffer: %+v", args)
@@ -43,7 +42,6 @@ func write(args []js.Value) ([]interface{}, error) {
*position = int64(args[4].Int())
}
- p := process.Current()
- n, err := p.Files().Write(fd, buffer, offset, length, position)
+ n, err := s.process.Files().Write(fd, buffer, offset, length, position)
return []interface{}{n, buffer}, err
}
diff --git a/internal/js/process/dir.go b/internal/js/process/dir.go
index e29cf96..e861bd0 100644
--- a/internal/js/process/dir.go
+++ b/internal/js/process/dir.go
@@ -5,18 +5,16 @@ package process
import (
"syscall/js"
- "github.com/hack-pad/hackpad/internal/process"
"github.com/pkg/errors"
)
-func cwd(args []js.Value) (interface{}, error) {
- return process.Current().WorkingDirectory(), nil
+func (s processShim) cwd(args []js.Value) (interface{}, error) {
+ return s.process.WorkingDirectory(), nil
}
-func chdir(args []js.Value) (interface{}, error) {
+func (s processShim) chdir(args []js.Value) (interface{}, error) {
if len(args) == 0 {
return nil, errors.New("a new directory argument is required")
}
- p := process.Current()
- return nil, p.SetWorkingDirectory(args[0].String())
+ return nil, s.process.SetWorkingDirectory(args[0].String())
}
diff --git a/internal/js/process/groups.go b/internal/js/process/groups.go
index 097cc96..679d993 100644
--- a/internal/js/process/groups.go
+++ b/internal/js/process/groups.go
@@ -9,14 +9,14 @@ const (
groupID = 0
)
-func geteuid(args []js.Value) (interface{}, error) {
+func (s processShim) geteuid(args []js.Value) (interface{}, error) {
return userID, nil
}
-func getegid(args []js.Value) (interface{}, error) {
+func (s processShim) getegid(args []js.Value) (interface{}, error) {
return groupID, nil
}
-func getgroups(args []js.Value) (interface{}, error) {
+func (s processShim) getgroups(args []js.Value) (interface{}, error) {
return groupID, nil
}
diff --git a/internal/js/process/process.go b/internal/js/process/process.go
index f687eae..f4d1800 100644
--- a/internal/js/process/process.go
+++ b/internal/js/process/process.go
@@ -5,46 +5,63 @@ package process
import (
"syscall/js"
+ "github.com/hack-pad/hackpad/internal/common"
"github.com/hack-pad/hackpad/internal/interop"
"github.com/hack-pad/hackpad/internal/process"
)
var jsProcess = js.Global().Get("process")
-func Init() {
- process.Init(switchedContext)
+type processShim struct {
+ process *process.Process
+ spawner Spawner
+ waiter Waiter
+}
+
+type PIDer interface {
+ PID() common.PID
+}
- currentProcess := process.Current()
- err := currentProcess.Files().MkdirAll(currentProcess.WorkingDirectory(), 0750)
+type Spawner interface {
+ Spawn(command string, argv []string, attr *process.ProcAttr) (PIDer, error)
+}
+
+type Waiter interface {
+ Wait(pid common.PID) (exitCode int, err error)
+}
+
+func Init(process *process.Process, spawner Spawner, waiter Waiter) {
+ shim := processShim{
+ process: process,
+ spawner: spawner,
+ waiter: waiter,
+ }
+
+ err := process.Files().MkdirAll(process.WorkingDirectory(), 0750) // TODO move to parent initialization
if err != nil {
panic(err)
}
globals := js.Global()
- interop.SetFunc(jsProcess, "getuid", geteuid)
- interop.SetFunc(jsProcess, "geteuid", geteuid)
- interop.SetFunc(jsProcess, "getgid", getegid)
- interop.SetFunc(jsProcess, "getegid", getegid)
- interop.SetFunc(jsProcess, "getgroups", getgroups)
- jsProcess.Set("pid", currentProcess.PID())
- jsProcess.Set("ppid", currentProcess.ParentPID())
- interop.SetFunc(jsProcess, "umask", umask)
- interop.SetFunc(jsProcess, "cwd", cwd)
- interop.SetFunc(jsProcess, "chdir", chdir)
+ interop.SetFunc(jsProcess, "getuid", shim.geteuid)
+ interop.SetFunc(jsProcess, "geteuid", shim.geteuid)
+ interop.SetFunc(jsProcess, "getgid", shim.getegid)
+ interop.SetFunc(jsProcess, "getegid", shim.getegid)
+ interop.SetFunc(jsProcess, "getgroups", shim.getgroups)
+ jsProcess.Set("pid", process.PID())
+ jsProcess.Set("ppid", process.ParentPID())
+ interop.SetFunc(jsProcess, "umask", shim.umask)
+ interop.SetFunc(jsProcess, "cwd", shim.cwd)
+ interop.SetFunc(jsProcess, "chdir", shim.chdir)
globals.Set("child_process", map[string]interface{}{})
childProcess := globals.Get("child_process")
- interop.SetFunc(childProcess, "spawn", spawn)
- //interop.SetFunc(childProcess, "spawnSync", spawnSync) // TODO is there any way to run spawnSync so we don't hit deadlock?
- interop.SetFunc(childProcess, "wait", wait)
- interop.SetFunc(childProcess, "waitSync", waitSync)
-}
-
-func switchedContext(pid, ppid process.PID) {
- jsProcess.Set("pid", pid)
- jsProcess.Set("ppid", ppid)
+ interop.SetFunc(childProcess, "spawn", shim.spawn)
+ //interop.SetFunc(childProcess, "spawnSync", shim.spawnSync) // TODO is there any way to run spawnSync so we don't hit deadlock?
+ interop.SetFunc(childProcess, "wait", shim.wait)
+ interop.SetFunc(childProcess, "waitSync", shim.waitSync)
}
-func Dump() interface{} {
+func (s processShim) Dump() interface{} {
return process.Dump()
}
diff --git a/internal/js/process/spawn.go b/internal/js/process/spawn.go
index 81c487b..198f244 100644
--- a/internal/js/process/spawn.go
+++ b/internal/js/process/spawn.go
@@ -11,7 +11,7 @@ import (
"github.com/pkg/errors"
)
-func spawn(args []js.Value) (interface{}, error) {
+func (s processShim) spawn(args []js.Value) (interface{}, error) {
if len(args) == 0 {
return nil, errors.Errorf("Invalid number of args, expected command name: %v", args)
}
@@ -32,15 +32,18 @@ func spawn(args []js.Value) (interface{}, error) {
if len(args) >= 3 {
argv[0], procAttr = parseProcAttr(command, args[2])
}
- return Spawn(command, argv, procAttr)
+ return s.Spawn(command, argv, procAttr)
}
-func Spawn(command string, args []string, attr *process.ProcAttr) (process.Process, error) {
- p, err := process.New(command, args, attr)
+func (s processShim) Spawn(command string, argv []string, attr *process.ProcAttr) (map[string]interface{}, error) {
+ pider, err := s.spawner.Spawn(command, argv, attr)
if err != nil {
- return p, err
+ return nil, err
}
- return p, p.Start()
+ return map[string]interface{}{
+ "pid": pider.PID(),
+ "ppid": s.process.PID(),
+ }, nil
}
func parseProcAttr(defaultCommand string, value js.Value) (argv0 string, attr *process.ProcAttr) {
diff --git a/internal/js/process/umask.go b/internal/js/process/umask.go
index 55b37d4..b2686b9 100644
--- a/internal/js/process/umask.go
+++ b/internal/js/process/umask.go
@@ -6,7 +6,7 @@ import "syscall/js"
var currentUMask = 0755
-func umask(args []js.Value) (interface{}, error) {
+func (s processShim) umask(args []js.Value) (interface{}, error) {
if len(args) == 0 {
return currentUMask, nil
}
diff --git a/internal/js/process/wait.go b/internal/js/process/wait.go
index 98375cf..1b2133a 100644
--- a/internal/js/process/wait.go
+++ b/internal/js/process/wait.go
@@ -10,32 +10,27 @@ import (
"github.com/pkg/errors"
)
-func wait(args []js.Value) ([]interface{}, error) {
- ret, err := waitSync(args)
+func (s processShim) wait(args []js.Value) ([]interface{}, error) {
+ ret, err := s.waitSync(args)
return []interface{}{ret}, err
}
-func waitSync(args []js.Value) (interface{}, error) {
+func (s processShim) waitSync(args []js.Value) (interface{}, error) {
if len(args) != 1 {
return nil, errors.Errorf("Invalid number of args, expected 1: %v", args)
}
pid := process.PID(args[0].Int())
waitStatus := new(syscall.WaitStatus)
- wpid, err := Wait(pid, waitStatus, 0, nil)
+ wpid, err := s.Wait(pid, waitStatus, 0, nil)
return map[string]interface{}{
"pid": wpid,
"exitCode": waitStatus.ExitStatus(),
}, err
}
-func Wait(pid process.PID, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid process.PID, err error) {
+func (s processShim) Wait(pid process.PID, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid process.PID, err error) {
// TODO support options and rusage
- p, ok := process.Get(pid)
- if !ok {
- return 0, errors.Errorf("Unknown child process: %d", pid)
- }
-
- exitCode, err := p.Wait()
+ exitCode, err := s.waiter.Wait(pid)
if wstatus != nil {
const (
// defined in syscall.WaitStatus
diff --git a/internal/jserror/error.go b/internal/jserror/error.go
new file mode 100644
index 0000000..d059611
--- /dev/null
+++ b/internal/jserror/error.go
@@ -0,0 +1,69 @@
+package jserror
+
+import (
+ "io"
+ "os/exec"
+
+ "github.com/hack-pad/hackpad/internal/log"
+ "github.com/hack-pad/hackpadfs"
+ "github.com/pkg/errors"
+)
+
+type Error interface {
+ error
+ Message() string
+ Code() string
+}
+
+type jsErr struct {
+ error
+ code string
+}
+
+func New(message, code string) Error {
+ return WrapErr(errors.New(message), code)
+}
+
+func WrapErr(err error, code string) Error {
+ return &jsErr{
+ error: err,
+ code: code,
+ }
+}
+
+func (e *jsErr) Message() string {
+ return e.Error()
+}
+
+func (e *jsErr) Code() string {
+ return e.code
+}
+
+// errno names pulled from syscall/tables_js.go
+func mapToErrNo(err error, debugMessage string) string {
+ if err, ok := err.(Error); ok {
+ return err.Code()
+ }
+ if err, ok := err.(interface{ Unwrap() error }); ok {
+ return mapToErrNo(err.Unwrap(), debugMessage)
+ }
+ switch err {
+ case io.EOF, exec.ErrNotFound:
+ return "ENOENT"
+ }
+ switch {
+ case errors.Is(err, hackpadfs.ErrClosed):
+ return "EBADF" // if it was already closed, then the file descriptor was invalid
+ case errors.Is(err, hackpadfs.ErrNotExist):
+ return "ENOENT"
+ case errors.Is(err, hackpadfs.ErrExist):
+ return "EEXIST"
+ case errors.Is(err, hackpadfs.ErrIsDir):
+ return "EISDIR"
+ case errors.Is(err, hackpadfs.ErrPermission):
+ return "EPERM"
+ default:
+ log.Errorf("Unknown error type: (%T) %+v\n\n%s", err, err, debugMessage)
+ return "EPERM"
+ }
+}
diff --git a/internal/interop/error_js.go b/internal/jserror/error_js.go
similarity index 69%
rename from internal/interop/error_js.go
rename to internal/jserror/error_js.go
index e1ca60c..400c550 100644
--- a/internal/interop/error_js.go
+++ b/internal/jserror/error_js.go
@@ -1,6 +1,6 @@
// +build js
-package interop
+package jserror
import (
"fmt"
@@ -9,11 +9,11 @@ import (
"github.com/pkg/errors"
)
-func WrapAsJSError(err error, message string) error {
- return wrapAsJSError(err, message)
+func Wrap(err error, message string) error {
+ return WrapArgs(err, message)
}
-func wrapAsJSError(err error, message string, args ...js.Value) error {
+func WrapArgs(err error, message string, args ...js.Value) error {
if err == nil {
return nil
}
diff --git a/internal/jsfunc/non_block.go b/internal/jsfunc/non_block.go
new file mode 100644
index 0000000..1b7fca1
--- /dev/null
+++ b/internal/jsfunc/non_block.go
@@ -0,0 +1,10 @@
+package jsfunc
+
+import "syscall/js"
+
+func NonBlocking(fn Func) js.Func {
+ return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ go fn(this, args)
+ return nil
+ })
+}
diff --git a/internal/jsfunc/promise.go b/internal/jsfunc/promise.go
new file mode 100644
index 0000000..8fbd691
--- /dev/null
+++ b/internal/jsfunc/promise.go
@@ -0,0 +1,25 @@
+package jsfunc
+
+import (
+ "syscall/js"
+
+ "github.com/hack-pad/hackpad/internal/jserror"
+ "github.com/hack-pad/hackpad/internal/promise"
+)
+
+type ErrFunc = func(this js.Value, args []js.Value) (js.Wrapper, error)
+
+func Promise(fn ErrFunc) js.Func {
+ return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ resolve, reject, prom := promise.New()
+ go func() {
+ value, err := fn(this, args)
+ if err != nil {
+ reject(jserror.Wrap(err, "Failed to install binary"))
+ return
+ }
+ resolve(value)
+ }()
+ return prom
+ })
+}
diff --git a/internal/interop/once_func.go b/internal/jsfunc/single_use.go
similarity index 64%
rename from internal/interop/once_func.go
rename to internal/jsfunc/single_use.go
index 9f24635..8e684ba 100644
--- a/internal/interop/once_func.go
+++ b/internal/jsfunc/single_use.go
@@ -1,10 +1,12 @@
// +build js
-package interop
+package jsfunc
import "syscall/js"
-func SingleUseFunc(fn func(this js.Value, args []js.Value) interface{}) js.Func {
+type Func = func(this js.Value, args []js.Value) interface{}
+
+func SingleUse(fn Func) js.Func {
var wrapperFn js.Func
wrapperFn = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
wrapperFn.Release()
diff --git a/internal/jsworker/local.go b/internal/jsworker/local.go
new file mode 100644
index 0000000..d515775
--- /dev/null
+++ b/internal/jsworker/local.go
@@ -0,0 +1,42 @@
+package jsworker
+
+import (
+ "context"
+ "syscall/js"
+)
+
+type Local struct {
+ port *MessagePort
+}
+
+var self *Local
+
+func init() {
+ jsSelf := js.Global().Get("self")
+ if !jsSelf.Truthy() {
+ return
+ }
+ port, err := WrapMessagePort(jsSelf)
+ if err != nil {
+ panic(err)
+ }
+ self = &Local{
+ port: port,
+ }
+}
+
+func GetLocal() *Local {
+ return self
+}
+
+func (l *Local) PostMessage(message js.Value, transfers []js.Value) error {
+ return l.port.PostMessage(message, transfers)
+}
+
+func (l *Local) Listen(ctx context.Context, listener func(MessageEvent, error)) error {
+ return l.port.Listen(ctx, listener)
+}
+
+func (l *Local) Close() error {
+ return l.port.Close()
+}
diff --git a/internal/jsworker/message_event.go b/internal/jsworker/message_event.go
new file mode 100644
index 0000000..c0897c5
--- /dev/null
+++ b/internal/jsworker/message_event.go
@@ -0,0 +1,30 @@
+package jsworker
+
+import (
+ "fmt"
+ "syscall/js"
+
+ "github.com/hack-pad/hackpad/internal/common"
+)
+
+type MessageEvent struct {
+ Data js.Value
+ Target *MessagePort
+}
+
+func parseMessageEvent(v js.Value) (_ MessageEvent, err error) {
+ defer common.CatchException(&err)
+ target, err := WrapMessagePort(v.Get("target"))
+ return MessageEvent{
+ Data: v.Get("data"),
+ Target: target,
+ }, err
+}
+
+type MessageEventErr struct {
+ MessageEvent
+}
+
+func (m MessageEventErr) Error() string {
+ return fmt.Sprintf("Failed to deserialize message: %+v", m.MessageEvent)
+}
diff --git a/internal/jsworker/message_port.go b/internal/jsworker/message_port.go
new file mode 100644
index 0000000..0b8c7c4
--- /dev/null
+++ b/internal/jsworker/message_port.go
@@ -0,0 +1,96 @@
+package jsworker
+
+import (
+ "context"
+ "runtime/debug"
+ "syscall/js"
+
+ "github.com/hack-pad/hackpad/internal/common"
+ "github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
+ "github.com/hack-pad/hackpad/internal/log"
+ "github.com/pkg/errors"
+)
+
+type MessagePort struct {
+ jsMessagePort js.Value
+}
+
+var jsMessageChannel = js.Global().Get("MessageChannel")
+
+func NewChannel() (port1, port2 *MessagePort, err error) {
+ defer common.CatchException(&err)
+ channel := jsMessageChannel.New()
+ port1, err = WrapMessagePort(channel.Get("port1"))
+ if err != nil {
+ return
+ }
+ port2, err = WrapMessagePort(channel.Get("port2"))
+ return
+}
+
+func WrapMessagePort(v js.Value) (*MessagePort, error) {
+ if !v.Get("postMessage").Truthy() {
+ return nil, errors.New("invalid MessagePort value: postMessage is not a function")
+ }
+ return &MessagePort{v}, nil
+}
+
+func (p *MessagePort) PostMessage(message js.Value, transfers []js.Value) (err error) {
+ defer common.CatchExceptionHandler(func(e error) {
+ err = e
+ log.Error(err, ": ", string(debug.Stack()))
+ })
+ args := append([]interface{}{message}, interop.SliceFromJSValues(transfers))
+ p.jsMessagePort.Call("postMessage", args...)
+ return nil
+}
+
+func (p *MessagePort) Listen(ctx context.Context, listener func(MessageEvent, error)) (err error) {
+ ctx, cancel := context.WithCancel(ctx)
+ defer common.CatchExceptionHandler(func(e error) {
+ err = e
+ cancel()
+ })
+
+ messageHandler := jsfunc.NonBlocking(func(this js.Value, args []js.Value) interface{} {
+ ev, err := parseMessageEvent(args[0])
+ listener(ev, err)
+ return nil
+ })
+ errorHandler := jsfunc.NonBlocking(func(this js.Value, args []js.Value) interface{} {
+ ev, err := parseMessageEvent(args[0])
+ if err == nil {
+ err = MessageEventErr{ev}
+ }
+ listener(MessageEvent{}, err)
+ return nil
+ })
+
+ go func() {
+ <-ctx.Done()
+ defer messageHandler.Release()
+ defer errorHandler.Release()
+ p.jsMessagePort.Call("removeEventListener", "message", messageHandler)
+ p.jsMessagePort.Call("removeEventListener", "messageerror", errorHandler)
+ }()
+ p.jsMessagePort.Call("addEventListener", "message", messageHandler)
+ p.jsMessagePort.Call("addEventListener", "messageerror", errorHandler)
+ if p.jsMessagePort.Get("start").Truthy() {
+ p.jsMessagePort.Call("start")
+ }
+ return nil
+}
+
+func (p *MessagePort) Close() (err error) {
+ defer common.CatchException(&err)
+ p.jsMessagePort.Call("close")
+ return nil
+}
+
+func (p *MessagePort) JSValue() js.Value {
+ if p == nil {
+ return js.Null()
+ }
+ return p.jsMessagePort
+}
diff --git a/internal/jsworker/remote.go b/internal/jsworker/remote.go
new file mode 100644
index 0000000..7c15937
--- /dev/null
+++ b/internal/jsworker/remote.go
@@ -0,0 +1,51 @@
+package jsworker
+
+import (
+ "context"
+ "syscall/js"
+
+ "github.com/hack-pad/hackpad/internal/common"
+)
+
+var (
+ jsWorker = js.Global().Get("Worker")
+)
+
+const (
+ wasmWorkerScript = "/wasmWorker.js"
+)
+
+type Remote struct {
+ port *MessagePort
+ worker js.Value
+}
+
+func NewRemote(name, url string) (_ *Remote, err error) {
+ defer common.CatchException(&err)
+ val := jsWorker.New(url, map[string]interface{}{
+ "name": name,
+ })
+ port, err := WrapMessagePort(val)
+ return &Remote{
+ port: port,
+ worker: val,
+ }, err
+}
+
+func NewRemoteWasm(name, wasmURL string) (*Remote, error) {
+ return NewRemote(name, wasmWorkerScript+"?wasm="+wasmURL)
+}
+
+func (r *Remote) Terminate() (err error) {
+ defer common.CatchException(&err)
+ r.worker.Call("terminate")
+ return nil
+}
+
+func (r *Remote) PostMessage(message js.Value, transfers []js.Value) error {
+ return r.port.PostMessage(message, transfers)
+}
+
+func (r *Remote) Listen(ctx context.Context, listener func(MessageEvent, error)) error {
+ return r.port.Listen(ctx, listener)
+}
diff --git a/internal/jsworker/types.go b/internal/jsworker/types.go
new file mode 100644
index 0000000..b48193d
--- /dev/null
+++ b/internal/jsworker/types.go
@@ -0,0 +1,35 @@
+package jsworker
+
+import (
+ "syscall/js"
+
+ "github.com/pkg/errors"
+)
+
+func jsInt(v js.Value) int {
+ if v.Type() != js.TypeNumber {
+ return 0
+ }
+ return v.Int()
+}
+
+func jsString(v js.Value) string {
+ if v.Type() != js.TypeString {
+ return ""
+ }
+ return v.String()
+}
+
+func jsBool(v js.Value) bool {
+ if v.Type() != js.TypeBoolean {
+ return false
+ }
+ return v.Bool()
+}
+
+func jsError(v js.Value) error {
+ if !v.Truthy() {
+ return nil
+ }
+ return errors.Errorf("%v", v)
+}
diff --git a/internal/kernel/kernel.go b/internal/kernel/kernel.go
new file mode 100644
index 0000000..cd1a0a2
--- /dev/null
+++ b/internal/kernel/kernel.go
@@ -0,0 +1,18 @@
+package kernel
+
+import (
+ "github.com/hack-pad/hackpad/internal/common"
+ "go.uber.org/atomic"
+)
+
+const (
+ minPID = 1
+)
+
+var (
+ lastPID = atomic.NewUint64(minPID)
+)
+
+func ReservePID() common.PID {
+ return common.PID(lastPID.Inc())
+}
diff --git a/internal/log/caller.go b/internal/log/caller.go
new file mode 100644
index 0000000..684582d
--- /dev/null
+++ b/internal/log/caller.go
@@ -0,0 +1,22 @@
+package log
+
+import (
+ "fmt"
+ "runtime"
+ "strings"
+)
+
+const (
+ hackpadCommonPrefix = "github.com/hack-pad/"
+)
+
+func getCaller(skip int) string {
+ pc, file, line, ok := runtime.Caller(skip + 1)
+ if !ok {
+ return ""
+ }
+ file = strings.TrimPrefix(file, hackpadCommonPrefix)
+ fn := runtime.FuncForPC(pc).Name()
+ fn = fn[strings.LastIndexAny(fn, "./")+1:]
+ return fmt.Sprintf("%s:%d:%s()", file, line, fn)
+}
diff --git a/internal/log/js_log.go b/internal/log/js_log.go
index d59c394..779bfce 100644
--- a/internal/log/js_log.go
+++ b/internal/log/js_log.go
@@ -34,29 +34,44 @@ func SetLevel(level consoleType) {
}
func DebugJSValues(args ...interface{}) int {
- return logJSValues(LevelDebug, args...)
+ return logJSValues(LevelDebug, 1, args...)
}
func PrintJSValues(args ...interface{}) int {
- return logJSValues(LevelLog, args...)
+ return logJSValues(LevelLog, 1, args...)
}
func WarnJSValues(args ...interface{}) int {
- return logJSValues(LevelWarn, args...)
+ return logJSValues(LevelWarn, 1, args...)
}
func ErrorJSValues(args ...interface{}) int {
- return logJSValues(LevelError, args...)
+ return logJSValues(LevelError, 1, args...)
}
-func logJSValues(kind consoleType, args ...interface{}) int {
+func logJSValues(kind consoleType, skip int, args ...interface{}) int {
if kind < logLevel {
return 0
}
- console.Call(kind.String(), args...)
+ caller := getCaller(skip + 1)
+ var newArgs []interface{}
+ if name := workerNamePrefix(); name != "" {
+ newArgs = append(newArgs, name)
+ }
+ newArgs = append(newArgs, caller)
+ newArgs = append(newArgs, args...)
+ console.Call(kind.String(), newArgs...)
return 0
}
func writeLog(c consoleType, s string) {
- console.Call(c.String(), s)
+ console.Call(c.String(), workerNamePrefix(), s)
+}
+
+func workerNamePrefix() string {
+ name := global.Get("workerName")
+ if name.Type() == js.TypeString {
+ return name.String() + ": "
+ }
+ return ""
}
diff --git a/internal/log/log.go b/internal/log/log.go
index f3eebf4..8af8026 100644
--- a/internal/log/log.go
+++ b/internal/log/log.go
@@ -1,6 +1,8 @@
package log
-import "fmt"
+import (
+ "fmt"
+)
type consoleType int
@@ -51,51 +53,57 @@ func parseLevel(level string) consoleType {
}
func Debugf(format string, args ...interface{}) int {
- return logf(LevelDebug, format, args...)
+ return logf(LevelDebug, 1, format, args...)
}
func Printf(format string, args ...interface{}) int {
- return logf(LevelLog, format, args...)
+ return logf(LevelLog, 1, format, args...)
}
func Warnf(format string, args ...interface{}) int {
- return logf(LevelWarn, format, args...)
+ return logf(LevelWarn, 1, format, args...)
}
func Errorf(format string, args ...interface{}) int {
- return logf(LevelError, format, args...)
+ return logf(LevelError, 1, format, args...)
}
-func logf(kind consoleType, format string, args ...interface{}) int {
+func logf(kind consoleType, skip int, format string, args ...interface{}) int {
if kind < logLevel {
return 0
}
s := fmt.Sprintf(format, args...)
+ if caller := getCaller(skip + 1); caller != "" {
+ s = caller + " - " + s
+ }
writeLog(kind, s)
return len(s)
}
func Debug(args ...interface{}) int {
- return log(LevelDebug, args...)
+ return log(LevelDebug, 1, args...)
}
func Print(args ...interface{}) int {
- return log(LevelLog, args...)
+ return log(LevelLog, 1, args...)
}
func Warn(args ...interface{}) int {
- return log(LevelWarn, args...)
+ return log(LevelWarn, 1, args...)
}
func Error(args ...interface{}) int {
- return log(LevelError, args...)
+ return log(LevelError, 1, args...)
}
-func log(kind consoleType, args ...interface{}) int {
+func log(kind consoleType, skip int, args ...interface{}) int {
if kind < logLevel {
return 0
}
s := fmt.Sprint(args...)
+ if caller := getCaller(skip + 1); caller != "" {
+ s = caller + " - " + s
+ }
writeLog(kind, s)
return len(s)
}
diff --git a/internal/process/context.go b/internal/process/context.go
deleted file mode 100644
index f21ec3f..0000000
--- a/internal/process/context.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package process
-
-import (
- "strings"
- "syscall"
-
- "github.com/hack-pad/hackpad/internal/fs"
- "github.com/hack-pad/hackpad/internal/log"
-)
-
-const initialDirectory = "/home/me"
-
-var (
- currentPID PID
-
- switchedContextListener func(newPID, parentPID PID)
-)
-
-func Init(switchedContext func(PID, PID)) {
- // create 'init' process
- fileDescriptors, err := fs.NewStdFileDescriptors(minPID, initialDirectory)
- if err != nil {
- panic(err)
- }
- p, err := newWithCurrent(
- &process{fileDescriptors: fileDescriptors},
- minPID,
- "",
- nil,
- &ProcAttr{Env: splitEnvPairs(syscall.Environ())},
- )
- if err != nil {
- panic(err)
- }
- p.state = stateRunning
- pids[minPID] = p
-
- switchedContextListener = switchedContext
- switchContext(minPID)
-}
-
-func switchContext(pid PID) (prev PID) {
- prev = currentPID
- log.Debug("Switching context from PID ", prev, " to ", pid)
- if pid == prev {
- return
- }
- newProcess := pids[pid]
- currentPID = pid
- switchedContextListener(pid, newProcess.parentPID)
- return
-}
-
-func Current() Process {
- process, _ := Get(currentPID)
- return process
-}
-
-func Get(pid PID) (process Process, ok bool) {
- p, ok := pids[pid]
- return p, ok
-}
-
-func splitEnvPairs(pairs []string) map[string]string {
- env := make(map[string]string)
- for _, pair := range pairs {
- equalIndex := strings.IndexRune(pair, '=')
- if equalIndex == -1 {
- env[pair] = ""
- } else {
- key, value := pair[:equalIndex], pair[equalIndex+1:]
- env[key] = value
- }
- }
- return env
-}
diff --git a/internal/process/process.go b/internal/process/process.go
index 3c0404c..26cd90a 100644
--- a/internal/process/process.go
+++ b/internal/process/process.go
@@ -3,7 +3,6 @@ package process
import (
"context"
"fmt"
- "os"
"sort"
"strings"
@@ -12,11 +11,6 @@ import (
"github.com/hack-pad/hackpad/internal/log"
"github.com/hack-pad/hackpadfs/keyvalue/blob"
"github.com/pkg/errors"
- "go.uber.org/atomic"
-)
-
-const (
- minPID = 1
)
type PID = common.PID
@@ -32,27 +26,15 @@ const (
)
var (
- pids = make(map[PID]*process)
- lastPID = atomic.NewUint64(minPID)
+ pids = make(map[PID]*Process)
)
-type Process interface {
- PID() PID
- ParentPID() PID
-
- Start() error
- Wait() (exitCode int, err error)
- Files() *fs.FileDescriptors
- WorkingDirectory() string
- SetWorkingDirectory(wd string) error
-}
-
-type process struct {
+type Process struct {
pid, parentPID PID
command string
args []string
state processState
- attr *ProcAttr
+ env map[string]string
ctx context.Context
ctxDone context.CancelFunc
exitCode int
@@ -61,23 +43,15 @@ type process struct {
setFilesWD func(wd string) error
}
-func New(command string, args []string, attr *ProcAttr) (Process, error) {
- return newWithCurrent(Current(), PID(lastPID.Inc()), command, args, attr)
-}
-
-func newWithCurrent(current Process, newPID PID, command string, args []string, attr *ProcAttr) (*process, error) {
- wd := current.WorkingDirectory()
- if attr.Dir != "" {
- wd = attr.Dir
- }
- files, setFilesWD, err := fs.NewFileDescriptors(newPID, wd, current.Files(), attr.Files)
+func New(newPID PID, command string, args []string, workingDirectory string, openFiles []common.OpenFileAttr, env map[string]string) (*Process, error) {
+ files, setFilesWD, err := fs.NewFileDescriptors(newPID, workingDirectory, openFiles)
ctx, cancel := context.WithCancel(context.Background())
- return &process{
+ return &Process{
pid: newPID,
command: command,
args: args,
state: statePending,
- attr: attr,
+ env: env,
ctx: ctx,
ctxDone: cancel,
err: err,
@@ -86,19 +60,19 @@ func newWithCurrent(current Process, newPID PID, command string, args []string,
}, err
}
-func (p *process) PID() PID {
+func (p *Process) PID() PID {
return p.pid
}
-func (p *process) ParentPID() PID {
+func (p *Process) ParentPID() PID {
return p.parentPID
}
-func (p *process) Files() *fs.FileDescriptors {
+func (p *Process) Files() *fs.FileDescriptors {
return p.fileDescriptors
}
-func (p *process) Start() error {
+func (p *Process) Start() error {
err := p.start()
if p.err == nil {
p.err = err
@@ -106,7 +80,7 @@ func (p *process) Start() error {
return p.err
}
-func (p *process) start() error {
+func (p *Process) start() error {
pids[p.pid] = p
log.Debugf("Spawning process: %v", p)
go func() {
@@ -120,9 +94,9 @@ func (p *process) start() error {
return nil
}
-func (p *process) prepExecutable() (command string, err error) {
+func (p *Process) prepExecutable() (command string, err error) {
fs := p.Files()
- command, err = lookPath(fs.Stat, os.Getenv("PATH"), p.command)
+ command, err = lookPath(fs.Stat, p.env["PATH"], p.command)
if err != nil {
return "", err
}
@@ -143,13 +117,13 @@ func (p *process) prepExecutable() (command string, err error) {
return command, nil
}
-func (p *process) Done() {
+func (p *Process) Done() {
log.Debug("PID ", p.pid, " is done.\n", p.fileDescriptors)
p.fileDescriptors.CloseAll()
p.ctxDone()
}
-func (p *process) handleErr(err error) {
+func (p *Process) handleErr(err error) {
p.state = stateDone
if err != nil {
log.Errorf("Failed to start process: %s", err.Error())
@@ -159,21 +133,21 @@ func (p *process) handleErr(err error) {
p.Done()
}
-func (p *process) Wait() (exitCode int, err error) {
+func (p *Process) Wait() (exitCode int, err error) {
<-p.ctx.Done()
return p.exitCode, p.err
}
-func (p *process) WorkingDirectory() string {
+func (p *Process) WorkingDirectory() string {
return p.Files().WorkingDirectory()
}
-func (p *process) SetWorkingDirectory(wd string) error {
+func (p *Process) SetWorkingDirectory(wd string) error {
return p.setFilesWD(wd)
}
-func (p *process) String() string {
- return fmt.Sprintf("PID=%s, Command=%v, State=%s, WD=%s, Attr=%+v, Err=%+v, Files:\n%v", p.pid, p.args, p.state, p.WorkingDirectory(), p.attr, p.err, p.fileDescriptors)
+func (p *Process) String() string {
+ return fmt.Sprintf("PID=%s, Command=%v, State=%s, WD=%s, Attr=%+v, Err=%+v, Files:\n%v", p.pid, p.args, p.state, p.WorkingDirectory(), p.env, p.err, p.fileDescriptors)
}
func Dump() interface{} {
@@ -190,3 +164,11 @@ func Dump() interface{} {
}
return s.String()
}
+
+func (p *Process) Env() map[string]string {
+ envCopy := make(map[string]string, len(p.env))
+ for k, v := range p.env {
+ envCopy[k] = v
+ }
+ return envCopy
+}
diff --git a/internal/process/process_js.go b/internal/process/process_js.go
index 3b152d7..32511da 100644
--- a/internal/process/process_js.go
+++ b/internal/process/process_js.go
@@ -6,20 +6,21 @@ import (
"syscall/js"
"github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jserror"
)
var (
jsGo = js.Global().Get("Go")
)
-func (p *process) JSValue() js.Value {
+func (p *Process) JSValue() js.Value {
return js.ValueOf(map[string]interface{}{
"pid": p.pid,
"ppid": p.parentPID,
- "error": interop.WrapAsJSError(p.err, "spawn"),
+ "error": jserror.Wrap(p.err, "spawn"),
})
}
-func (p *process) StartCPUProfile() error {
+func (p *Process) StartCPUProfile() error {
return interop.StartCPUProfile(p.ctx)
}
diff --git a/internal/process/process_other.go b/internal/process/process_other.go
index 867ede1..dd1cf50 100644
--- a/internal/process/process_other.go
+++ b/internal/process/process_other.go
@@ -8,7 +8,7 @@ import (
"os/exec"
)
-func (p *process) run(path string) {
+func (p *Process) run(path string) {
cmd := exec.Command(path, p.args...)
if p.attr.Env == nil {
cmd.Env = os.Environ()
diff --git a/internal/process/wasm.go b/internal/process/wasm.go
index 058a7a1..4a4b203 100644
--- a/internal/process/wasm.go
+++ b/internal/process/wasm.go
@@ -5,9 +5,11 @@ package process
import (
"os"
"runtime"
+ "strings"
"syscall/js"
"github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
"github.com/hack-pad/hackpad/internal/log"
"github.com/hack-pad/hackpad/internal/promise"
)
@@ -16,11 +18,11 @@ var (
jsObject = js.Global().Get("Object")
)
-func (p *process) newWasmInstance(path string, importObject js.Value) (js.Value, error) {
+func (p *Process) newWasmInstance(path string, importObject js.Value) (js.Value, error) {
return p.Files().WasmInstance(path, importObject)
}
-func (p *process) run(path string) {
+func (p *Process) run(path string) {
defer func() {
go runtime.GC()
}()
@@ -36,20 +38,18 @@ func (p *process) run(path string) {
p.handleErr(err)
}
-func (p *process) startWasmPromise(path string, exitChan chan<- int) (promise.Promise, error) {
+func (p *Process) startWasmPromise(path string, exitChan chan<- int) (promise.Promise, error) {
p.state = stateCompiling
goInstance := jsGo.New()
goInstance.Set("argv", interop.SliceFromStrings(p.args))
- if p.attr.Env == nil {
- p.attr.Env = splitEnvPairs(os.Environ())
+ if p.env == nil {
+ p.env = splitEnvPairs(os.Environ())
}
- goInstance.Set("env", interop.StringMap(p.attr.Env))
- var resumeFuncPtr *js.Func
- goInstance.Set("exit", interop.SingleUseFunc(func(this js.Value, args []js.Value) interface{} {
+ goInstance.Set("env", interop.StringMap(p.env))
+ goInstance.Set("exit", jsfunc.SingleUse(func(this js.Value, args []js.Value) interface{} {
defer func() {
- if resumeFuncPtr != nil {
- resumeFuncPtr.Release()
- }
+ // TODO exit hook for worker
+
// TODO free the whole goInstance to fix garbage issues entirely. Freeing individual properties appears to work for now, but is ultimately a bad long-term solution because memory still accumulates.
goInstance.Set("mem", js.Null())
goInstance.Set("importObject", js.Null())
@@ -72,40 +72,20 @@ func (p *process) startWasmPromise(path string, exitChan chan<- int) (promise.Pr
return nil, err
}
- exports := instance.Get("exports")
+ p.state = stateRunning
+ return promise.From(goInstance.Call("run", instance)), nil
+}
- resumeFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
- defer interop.PanicLogger()
- prev := switchContext(p.pid)
- ret := exports.Call("resume", interop.SliceFromJSValues(args)...)
- switchContext(prev)
- return ret
- })
- resumeFuncPtr = &resumeFunc
- wrapperExports := map[string]interface{}{
- "run": interop.SingleUseFunc(func(this js.Value, args []js.Value) interface{} {
- defer interop.PanicLogger()
- prev := switchContext(p.pid)
- ret := exports.Call("run", interop.SliceFromJSValues(args)...)
- switchContext(prev)
- return ret
- }),
- "resume": resumeFunc,
- }
- for export, value := range interop.Entries(exports) {
- _, overridden := wrapperExports[export]
- if !overridden {
- wrapperExports[export] = value
+func splitEnvPairs(pairs []string) map[string]string {
+ env := make(map[string]string)
+ for _, pair := range pairs {
+ equalIndex := strings.IndexRune(pair, '=')
+ if equalIndex == -1 {
+ env[pair] = ""
+ } else {
+ key, value := pair[:equalIndex], pair[equalIndex+1:]
+ env[key] = value
}
}
- wrapperInstance := jsObject.Call("defineProperty",
- jsObject.Call("create", instance),
- "exports", map[string]interface{}{ // Instance.exports is read-only, so create a shim
- "value": wrapperExports,
- "writable": false,
- },
- )
-
- p.state = stateRunning
- return promise.From(goInstance.Call("run", wrapperInstance)), nil
+ return env
}
diff --git a/internal/promise/js.go b/internal/promise/js.go
index 88fe6b7..c0d00ba 100644
--- a/internal/promise/js.go
+++ b/internal/promise/js.go
@@ -6,7 +6,6 @@ import (
"runtime/debug"
"syscall/js"
- "github.com/hack-pad/hackpad/internal/interop"
"github.com/hack-pad/hackpad/internal/log"
)
@@ -23,7 +22,7 @@ func From(promiseValue js.Value) JS {
func New() (resolve, reject Resolver, promise JS) {
resolvers := make(chan Resolver, 2)
promise = From(
- jsPromise.New(interop.SingleUseFunc(func(this js.Value, args []js.Value) interface{} {
+ jsPromise.New(singleUseFunc(func(this js.Value, args []js.Value) interface{} {
resolve, reject := args[0], args[1]
resolvers <- func(result interface{}) { resolve.Invoke(result) }
resolvers <- func(result interface{}) { reject.Invoke(result) }
@@ -34,13 +33,22 @@ func New() (resolve, reject Resolver, promise JS) {
return
}
+func singleUseFunc(fn func(this js.Value, args []js.Value) interface{}) js.Func {
+ var wrapperFn js.Func
+ wrapperFn = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ wrapperFn.Release()
+ return fn(this, args)
+ })
+ return wrapperFn
+}
+
func (p JS) Then(fn func(value interface{}) interface{}) Promise {
return p.do("then", fn)
}
func (p JS) do(methodName string, fn func(value interface{}) interface{}) Promise {
return JS{
- value: p.value.Call(methodName, interop.SingleUseFunc(func(this js.Value, args []js.Value) interface{} {
+ value: p.value.Call(methodName, singleUseFunc(func(this js.Value, args []js.Value) interface{} {
var value js.Value
if len(args) > 0 {
value = args[0]
diff --git a/internal/terminal/term.go b/internal/terminal/term.go
index f717d32..9b751a1 100644
--- a/internal/terminal/term.go
+++ b/internal/terminal/term.go
@@ -1,3 +1,4 @@
+//go:build js
// +build js
package terminal
@@ -5,8 +6,11 @@ package terminal
import (
"syscall/js"
+ "github.com/hack-pad/hackpad/internal/common"
"github.com/hack-pad/hackpad/internal/fs"
"github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
+ "github.com/hack-pad/hackpad/internal/kernel"
"github.com/hack-pad/hackpad/internal/log"
"github.com/hack-pad/hackpad/internal/process"
"github.com/hack-pad/hackpadfs/indexeddb/idbblob"
@@ -14,21 +18,20 @@ import (
)
func SpawnTerminal(this js.Value, args []js.Value) interface{} {
- go func() {
- defer func() {
- if r := recover(); r != nil {
- log.Error("Recovered from panic:", r)
- }
- }()
- err := Open(args)
- if err != nil {
- log.Error(err)
+ defer func() {
+ if r := recover(); r != nil {
+ log.Error("Recovered from panic:", r)
}
}()
+ err := Open(args)
+ if err != nil {
+ log.Error(err)
+ }
return nil
}
func Open(args []js.Value) error {
+ return errors.New("not implemented")
if len(args) != 2 {
return errors.New("Invalid number of args for spawnTerminal. Expected 2: term, options")
}
@@ -50,19 +53,22 @@ func Open(args []js.Value) error {
workingDirectory = wd.String()
}
- files := process.Current().Files()
- stdinR, stdinW := pipe(files)
- stdoutR, stdoutW := pipe(files)
- stderrR, stderrW := pipe(files)
+ var files *fs.FileDescriptors
+ panic("not implemented")
+ //files := process.Current().Files()
+ //stdinR, stdinW := pipe(files)
+ //stdoutR, stdoutW := pipe(files)
+ //stderrR, stderrW := pipe(files)
+ var stdoutR, stderrR, stdinW common.FID
- proc, err := process.New(procArgs[0], procArgs, &process.ProcAttr{
+ proc, err := process.New(kernel.ReservePID(), procArgs[0], procArgs, workingDirectory, nil, nil) /*&process.ProcAttr{
Dir: workingDirectory,
Files: []fs.Attr{
{FID: stdinR},
{FID: stdoutW},
{FID: stderrW},
},
- })
+ }*/
if err != nil {
return err
}
@@ -71,7 +77,7 @@ func Open(args []js.Value) error {
return err
}
- f := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+ f := jsfunc.NonBlocking(func(this js.Value, args []js.Value) interface{} {
chunk, err := idbblob.New(args[0])
if err != nil {
log.Error("blob: Failed to write to terminal:", err)
diff --git a/internal/worker/dom.go b/internal/worker/dom.go
new file mode 100644
index 0000000..61bb58a
--- /dev/null
+++ b/internal/worker/dom.go
@@ -0,0 +1,48 @@
+package worker
+
+import (
+ "context"
+ "syscall/js"
+
+ "github.com/hack-pad/hackpad/internal/jsworker"
+)
+
+type DOM struct {
+ local *Local
+ port *jsworker.Local
+}
+
+func ExecDOM(ctx context.Context, localJS *jsworker.Local, command string, args []string, workingDirectory string, env map[string]string) (*DOM, error) {
+ msg, transfers := makeInitMessage(
+ "dom",
+ command, append([]string{command}, args...),
+ workingDirectory,
+ env,
+ nil,
+ )
+ err := localJS.PostMessage(msg, transfers)
+ if err != nil {
+ return nil, err
+ }
+ local, err := NewLocal(ctx, localJS)
+ if err != nil {
+ return nil, err
+ }
+ if err := local.Start(); err != nil {
+ return nil, err
+ }
+ return &DOM{
+ local: local,
+ port: localJS,
+ }, nil
+}
+
+func (d *DOM) Start() error {
+ return d.port.PostMessage(makeStartMessage(), nil)
+}
+
+func makeStartMessage() js.Value {
+ return js.ValueOf(map[string]interface{}{
+ "start": true,
+ })
+}
diff --git a/internal/worker/local.go b/internal/worker/local.go
new file mode 100644
index 0000000..85eea7b
--- /dev/null
+++ b/internal/worker/local.go
@@ -0,0 +1,217 @@
+package worker
+
+import (
+ "context"
+ "io"
+ "syscall/js"
+
+ "github.com/hack-pad/hackpad/internal/common"
+ "github.com/hack-pad/hackpad/internal/global"
+ "github.com/hack-pad/hackpad/internal/interop"
+ jsfs "github.com/hack-pad/hackpad/internal/js/fs"
+ jsprocess "github.com/hack-pad/hackpad/internal/js/process"
+ "github.com/hack-pad/hackpad/internal/jsworker"
+ "github.com/hack-pad/hackpad/internal/kernel"
+ "github.com/hack-pad/hackpad/internal/log"
+ "github.com/hack-pad/hackpad/internal/process"
+ "github.com/pkg/errors"
+)
+
+type Local struct {
+ localJS *jsworker.Local
+ process *process.Process
+ processStartCtx context.Context
+ pids map[common.PID]*Remote
+}
+
+func NewLocal(ctx context.Context, localJS *jsworker.Local) (_ *Local, err error) {
+ local := &Local{
+ localJS: localJS,
+ pids: make(map[common.PID]*Remote),
+ }
+ init, err := local.awaitInit(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer common.CatchException(&err)
+
+ global.Set("workerName", init.Get("workerName"))
+ log.Debug("Setting process details...")
+ var (
+ command = init.Get("command")
+ argv = init.Get("argv")
+ workingDirectory = init.Get("workingDirectory")
+ openFiles = init.Get("openFiles")
+ env = init.Get("env")
+ )
+ local.process, err = process.New(
+ kernel.ReservePID(),
+ command.String(),
+ interop.StringsFromJSValue(argv),
+ workingDirectory.String(),
+ parseOpenFiles(openFiles),
+ interop.StringMapFromJSObject(env),
+ )
+ if err != nil {
+ return nil, err
+ }
+ log.Debug("Initializing process")
+ jsprocess.Init(local.process, local, local)
+ log.Debug("Initializing fs")
+ jsfs.Init(local.process)
+ global.Set("process", map[string]interface{}{
+ "command": command,
+ "argv": argv,
+ "workingDirectory": workingDirectory,
+ "env": env,
+ })
+ return local, nil
+}
+
+func (l *Local) awaitInit(ctx context.Context) (js.Value, error) {
+ log.Debug("NewLocal 1")
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ l.processStartCtx = ctx
+
+ type initMessage struct {
+ err error
+ init js.Value
+ }
+ initChan := make(chan initMessage, 1)
+ err := l.localJS.Listen(ctx, func(me jsworker.MessageEvent, err error) {
+ if err != nil {
+ initChan <- initMessage{err: err}
+ return
+ }
+ if !me.Data.Truthy() || me.Data.Type() != js.TypeObject {
+ return
+ }
+ initData := me.Data.Get("init")
+ if !initData.Truthy() {
+ return
+ }
+ initChan <- initMessage{init: initData}
+ })
+ if err != nil {
+ return js.Value{}, err
+ }
+ err = l.localJS.PostMessage(js.ValueOf("pending_init"), nil)
+ if err != nil {
+ return js.Value{}, err
+ }
+ log.Debug("NewLocal 2")
+ message := <-initChan
+ log.Debug("NewLocal 3")
+ return message.init, message.err
+}
+
+func (l *Local) Start() (err error) {
+ defer common.CatchException(&err)
+ startCtx, cancel := context.WithCancel(context.Background())
+ err = l.localJS.Listen(startCtx, func(me jsworker.MessageEvent, err error) {
+ if err != nil {
+ log.Error(err)
+ cancel()
+ return
+ }
+ defer common.CatchExceptionHandler(func(err error) {
+ log.Error(err)
+ cancel()
+ })
+ if me.Data.Type() != js.TypeObject {
+ return
+ }
+ entries := interop.Entries(me.Data)
+ _, ok := entries["start"]
+ if !ok {
+ return
+ }
+ cancel()
+
+ err = l.process.Start()
+ if err != nil {
+ log.Error(err)
+ return
+ }
+ })
+ if err != nil {
+ return err
+ }
+
+ global.Set("ready", true)
+ log.Debug("before ready post")
+ err = l.localJS.PostMessage(js.ValueOf("ready"), nil)
+ if err != nil {
+ return err
+ }
+ log.Debug("after ready post")
+ return nil
+}
+
+func (l *Local) Exit(exitCode int) error {
+ err := l.localJS.PostMessage(makeExitMessage(exitCode), nil)
+ if err != nil {
+ return err
+ }
+ return l.localJS.Close()
+}
+
+func (l *Local) Spawn(command string, argv []string, attr *process.ProcAttr) (jsprocess.PIDer, error) {
+ pid := kernel.ReservePID()
+ log.Debug("Spawning pid ", pid, " for command: ", command, argv)
+ remote, err := NewRemote(l, pid, command, argv, attr)
+ if err != nil {
+ return nil, err
+ }
+ l.pids[pid] = remote
+ return remote, nil
+}
+
+func (l *Local) Wait(pid common.PID) (exitCode int, err error) {
+ log.Debug("Waiting on pid ", pid)
+ if pid == l.process.PID() {
+ return l.process.Wait()
+ }
+ remote, ok := l.pids[pid]
+ if !ok {
+ return 0, errors.Errorf("Unknown child process: %d", pid)
+ }
+ return remote.Wait()
+}
+
+func (l *Local) Started() <-chan struct{} {
+ return l.processStartCtx.Done()
+}
+
+func (l *Local) PID() common.PID {
+ return l.process.PID()
+}
+
+func makeExitMessage(exitCode int) js.Value {
+ return js.ValueOf(map[string]interface{}{
+ "exitCode": exitCode,
+ })
+}
+
+func parseOpenFiles(v js.Value) []common.OpenFileAttr {
+ openFileJSValues := interop.SliceFromJSValue(v)
+ var openFiles []common.OpenFileAttr
+ for _, o := range openFileJSValues {
+ openFile := readOpenFile(o)
+ var pipe io.ReadWriteCloser
+ if openFile.pipe != nil {
+ var err error
+ pipe, err = portToReadWriteCloser(openFile.pipe)
+ if err != nil {
+ panic(err)
+ }
+ }
+ openFiles = append(openFiles, common.OpenFileAttr{
+ FilePath: openFile.filePath,
+ SeekOffset: openFile.seekOffset,
+ RawDevice: pipe,
+ })
+ }
+ return openFiles
+}
diff --git a/internal/worker/open_file.go b/internal/worker/open_file.go
new file mode 100644
index 0000000..5c74bf9
--- /dev/null
+++ b/internal/worker/open_file.go
@@ -0,0 +1,51 @@
+package worker
+
+import (
+ "syscall/js"
+
+ "github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jsworker"
+)
+
+const (
+ ofFilePath = "filePath"
+ ofSeekOffset = "seekOffset"
+ ofPipe = "pipe"
+)
+
+type openFile struct {
+ filePath string
+ seekOffset int64
+ pipe *jsworker.MessagePort
+}
+
+func readOpenFile(v js.Value) openFile {
+ props := interop.Entries(v)
+ return openFile{
+ filePath: optionalString(props[ofFilePath]),
+ seekOffset: optionalInt64(props[ofSeekOffset]),
+ pipe: optionalPipe(props[ofPipe]),
+ }
+}
+
+func (o openFile) JSValue() js.Value {
+ return js.ValueOf(map[string]interface{}{
+ ofFilePath: o.filePath,
+ ofSeekOffset: o.seekOffset,
+ ofPipe: o.pipe.JSValue(),
+ })
+}
+
+func optionalString(v js.Value) string {
+ if v.Type() != js.TypeString {
+ return ""
+ }
+ return v.String()
+}
+
+func optionalInt64(v js.Value) int64 {
+ if v.Type() != js.TypeNumber {
+ return 0
+ }
+ return int64(v.Int())
+}
diff --git a/internal/worker/pipe.go b/internal/worker/pipe.go
new file mode 100644
index 0000000..c84e42c
--- /dev/null
+++ b/internal/worker/pipe.go
@@ -0,0 +1,83 @@
+package worker
+
+import (
+ "context"
+ "io"
+ "syscall/js"
+
+ "github.com/hack-pad/hackpad/internal/jsworker"
+ "github.com/hack-pad/hackpadfs/indexeddb/idbblob"
+ "github.com/hack-pad/hackpadfs/keyvalue/blob"
+)
+
+func optionalPipe(v js.Value) *jsworker.MessagePort {
+ if v.Type() != js.TypeObject {
+ return nil
+ }
+ port, err := jsworker.WrapMessagePort(v)
+ if err != nil {
+ panic(err)
+ }
+ return port
+}
+
+type portPipe struct {
+ port *jsworker.MessagePort
+ receivedData <-chan portPipeMessage
+ remainingReadData []byte
+ cancel context.CancelFunc
+}
+
+type portPipeMessage struct {
+ Data []byte
+ Err error
+}
+
+func portToReadWriteCloser(port *jsworker.MessagePort) (io.ReadWriteCloser, error) {
+ ctx, cancel := context.WithCancel(context.Background())
+ receivedData := make(chan portPipeMessage)
+ err := port.Listen(ctx, func(me jsworker.MessageEvent, err error) {
+ var buf []byte
+ if err == nil {
+ var bl blob.Blob
+ bl, err = idbblob.New(me.Data)
+ if err == nil {
+ buf = bl.Bytes()
+ }
+ }
+ receivedData <- portPipeMessage{Data: buf, Err: err}
+ })
+ if err != nil {
+ return nil, err
+ }
+ return &portPipe{
+ port: port,
+ receivedData: receivedData,
+ cancel: cancel,
+ }, nil
+}
+
+func (p *portPipe) Close() error {
+ p.cancel()
+ return p.port.Close()
+}
+
+func (p *portPipe) Read(b []byte) (n int, err error) {
+ if len(p.remainingReadData) == 0 {
+ message := <-p.receivedData
+ p.remainingReadData = message.Data
+ err = message.Err
+ }
+ n = copy(b, p.remainingReadData)
+ p.remainingReadData = p.remainingReadData[n:]
+ return
+}
+
+func (p *portPipe) Write(b []byte) (n int, err error) {
+ bl := idbblob.FromBlob(blob.NewBytes(b)).JSValue()
+ err = p.port.PostMessage(bl, []js.Value{bl.Get("buffer")})
+ if err == nil {
+ n = len(b)
+ }
+ return
+}
diff --git a/internal/worker/remote.go b/internal/worker/remote.go
new file mode 100644
index 0000000..1e5c110
--- /dev/null
+++ b/internal/worker/remote.go
@@ -0,0 +1,243 @@
+package worker
+
+import (
+ "context"
+ "fmt"
+ "syscall/js"
+
+ "github.com/hack-pad/hackpad/internal/common"
+ "github.com/hack-pad/hackpad/internal/interop"
+ "github.com/hack-pad/hackpad/internal/jsworker"
+ "github.com/hack-pad/hackpad/internal/log"
+ "github.com/hack-pad/hackpad/internal/process"
+ "github.com/hack-pad/hackpadfs"
+ "github.com/hack-pad/hackpadfs/indexeddb/idbblob"
+ "github.com/hack-pad/hackpadfs/keyvalue/blob"
+)
+
+type Remote struct {
+ pid common.PID
+ port *jsworker.Remote
+ closeCtx context.Context
+ closeExitCode *int
+ closeErr error
+}
+
+func NewRemote(local *Local, pid process.PID, command string, argv []string, attr *process.ProcAttr) (*Remote, error) {
+ ctx := context.Background()
+ closeCtx, cancel := context.WithCancel(ctx)
+
+ // collect current process details and use as defaults, remote workers won't inherit them
+ if attr.Dir == "" {
+ attr.Dir = local.process.WorkingDirectory()
+ }
+ if len(attr.Env) == 0 {
+ attr.Env = local.process.Env()
+ }
+
+ var openFiles []openFile
+ for _, f := range attr.Files {
+ file, err := local.process.Files().OpenRawFID(pid, f.FID)
+ if err != nil {
+ return nil, err
+ }
+ info, err := file.Stat()
+ if err != nil {
+ return nil, err
+ }
+ openF := openFile{
+ filePath: info.Name(),
+ seekOffset: 0, // TODO expose seek offset in file descriptor
+ }
+ if info.Mode()&hackpadfs.ModeNamedPipe != 0 {
+ port1, port2, err := jsworker.NewChannel()
+ if err != nil {
+ return nil, err
+ }
+ openF.pipe = port1
+ err = bindPortToFile(closeCtx, port2, file)
+ if err != nil {
+ return nil, err
+ }
+ }
+ openFiles = append(openFiles, openF)
+ }
+ workerName := fmt.Sprintf("pid-%d", pid)
+ port, err := jsworker.NewRemoteWasm(workerName, "/wasm/worker.wasm")
+ if err != nil {
+ return nil, err
+ }
+
+ remote := &Remote{
+ pid: pid,
+ port: port,
+ closeCtx: closeCtx,
+ }
+
+ err = port.Listen(closeCtx, func(me jsworker.MessageEvent, err error) {
+ if err != nil {
+ remote.closeErr = err
+ cancel()
+ return
+ }
+ if me.Data.Type() != js.TypeObject {
+ return
+ }
+ data := interop.Entries(me.Data)
+ if jsExitCode, ok := data["exitCode"]; ok && jsExitCode.Type() == js.TypeNumber {
+ exitCode := jsExitCode.Int()
+ remote.closeExitCode = &exitCode
+ cancel()
+ log.Debug("Remote exited with code:", exitCode)
+ }
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ go func() {
+ log.Debug("Worker ", workerName, " awaiting pending_init...")
+ err := awaitMessage(ctx, port, "pending_init")
+ if err != nil {
+ log.Error("Failed awaiting pending_init:", workerName, err)
+ return
+ }
+ log.Debug("Worker ", workerName, " waiting to init. Sending init...")
+ msg, transfers := makeInitMessage(workerName, command, argv, attr.Dir, attr.Env, openFiles)
+ err = port.PostMessage(msg, transfers)
+ if err != nil {
+ log.Error("Failed sending init to worker: ", workerName, " ", err)
+ return
+ }
+ log.Debug("Sent init message to worker ", workerName, ". Awaiting ready...")
+ if err := awaitMessage(ctx, remote.port, "ready"); err != nil {
+ log.Error("Failed awaiting ready:", workerName, err)
+ return
+ }
+ log.Debug("Worker ", workerName, " is ready. Sending start message.")
+ err = remote.port.PostMessage(makeStartMessage(), nil)
+ if err != nil {
+ log.Error("Failed sending start to worker: ", workerName, " ", err)
+ return
+ }
+ log.Debug("Sent start message.")
+ }()
+
+ return remote, nil
+}
+
+func (r *Remote) PID() common.PID {
+ return r.pid
+}
+
+func awaitMessage(ctx context.Context, port *jsworker.Remote, messageStr string) error {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ result := make(chan error, 1)
+ err := port.Listen(ctx, func(me jsworker.MessageEvent, err error) {
+ if err != nil {
+ result <- err
+ return
+ }
+ if me.Data.Type() == js.TypeString && me.Data.String() == messageStr {
+ result <- nil
+ }
+ })
+ if err != nil {
+ return err
+ }
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case err := <-result:
+ return err
+ }
+}
+
+func makeInitMessage(
+ workerName,
+ command string, argv []string,
+ workingDirectory string,
+ env map[string]string,
+ openFiles []openFile,
+) (msg js.Value, transfers []js.Value) {
+ var openFileJSValues []interface{}
+ var ports []js.Value
+ for _, o := range openFiles {
+ openFileJSValues = append(openFileJSValues, o)
+ if o.pipe != nil {
+ ports = append(ports, o.pipe.JSValue())
+ }
+ }
+ return js.ValueOf(map[string]interface{}{
+ "init": map[string]interface{}{
+ "workerName": workerName,
+ "command": command,
+ "argv": interop.SliceFromStrings(argv),
+ "workingDirectory": workingDirectory,
+ "env": interop.StringMap(env),
+ "openFiles": openFileJSValues,
+ },
+ }), ports
+}
+
+func (r *Remote) Wait() (exitCode int, err error) {
+ <-r.closeCtx.Done()
+ if r.closeExitCode == nil {
+ switch {
+ case r.closeErr != nil:
+ return 0, r.closeErr
+ default:
+ return 0, r.closeCtx.Err()
+ }
+ }
+ return *r.closeExitCode, r.closeErr
+}
+
+func bindPortToFile(ctx context.Context, port *jsworker.MessagePort, file hackpadfs.File) error {
+ err := port.Listen(ctx, func(me jsworker.MessageEvent, err error) {
+ if err != nil {
+ log.Error(err)
+ return
+ }
+ bl, err := idbblob.New(me.Data)
+ if err != nil {
+ log.Error(err)
+ return
+ }
+ _, err = hackpadfs.WriteFile(file, bl.Bytes())
+ if err != nil {
+ log.Error(err)
+ return
+ }
+ })
+ if err != nil {
+ return err
+ }
+ go func() {
+ <-ctx.Done()
+ file.Close()
+ }()
+ go func() {
+ const maxReadSize = 1 << 10
+ buf := make([]byte, maxReadSize)
+ for {
+ n, err := file.Read(buf)
+ if err != nil {
+ if err.Error() != "operation not supported" {
+ log.Error(err)
+ }
+ return
+ }
+ if n > 0 {
+ bl := idbblob.FromBlob(blob.NewBytes(buf[:n])).JSValue()
+ err := port.PostMessage(bl, []js.Value{bl.Get("buffer")})
+ if err != nil {
+ log.Error(err)
+ return
+ }
+ }
+ }
+ }()
+ return nil
+}
diff --git a/main.go b/main.go
index 6b9661e..8ef422b 100644
--- a/main.go
+++ b/main.go
@@ -1,45 +1,153 @@
+//go:build js
// +build js
package main
import (
- "path/filepath"
- "syscall/js"
+ "context"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "runtime/debug"
+ "github.com/hack-pad/go-indexeddb/idb"
+ "github.com/hack-pad/hackpad/internal/common"
+ "github.com/hack-pad/hackpad/internal/fs"
"github.com/hack-pad/hackpad/internal/global"
"github.com/hack-pad/hackpad/internal/interop"
- "github.com/hack-pad/hackpad/internal/js/fs"
- "github.com/hack-pad/hackpad/internal/js/process"
+ "github.com/hack-pad/hackpad/internal/jsfunc"
+ "github.com/hack-pad/hackpad/internal/jsworker"
"github.com/hack-pad/hackpad/internal/log"
- libProcess "github.com/hack-pad/hackpad/internal/process"
"github.com/hack-pad/hackpad/internal/terminal"
+ "github.com/hack-pad/hackpad/internal/worker"
+ "github.com/hack-pad/hackpadfs"
+ "github.com/hack-pad/hackpadfs/indexeddb"
+ "github.com/johnstarich/go/datasize"
)
+type domShim struct {
+ dom *worker.DOM
+}
+
func main() {
- process.Init()
- fs.Init()
- global.Set("spawnTerminal", js.FuncOf(terminal.SpawnTerminal))
- global.Set("dump", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
- go func() {
- basePath := ""
- if len(args) >= 1 {
- basePath = args[0].String()
- if filepath.IsAbs(basePath) {
- basePath = filepath.Clean(basePath)
- } else {
- basePath = filepath.Join(libProcess.Current().WorkingDirectory(), basePath)
- }
- }
- var fsDump interface{}
- if basePath != "" {
- fsDump = fs.Dump(basePath)
- }
- log.Error("Process:\n", process.Dump(), "\n\nFiles:\n", fsDump)
- }()
- return nil
- }))
- global.Set("profile", js.FuncOf(interop.ProfileJS))
- global.Set("install", js.FuncOf(installFunc))
- interop.SetInitialized()
+ defer common.CatchExceptionHandler(func(err error) {
+ log.Error("Hackpad panic:", err, "\n", string(debug.Stack()))
+ os.Exit(1)
+ })
+
+ bootCtx := context.Background()
+ //bootCtx, bootCancel := context.WithTimeout(context.Background(), 30*time.Second)
+ //defer bootCancel()
+ dom, err := worker.ExecDOM(
+ bootCtx,
+ jsworker.GetLocal(),
+ "editor",
+ []string{"-editor=editor"},
+ "/home/me",
+ map[string]string{
+ "GOMODCACHE": "/home/me/.cache/go-mod",
+ "GOPROXY": "https://proxy.golang.org/",
+ "GOROOT": "/usr/local/go",
+ "HOME": "/home/me",
+ "PATH": "/bin:/home/me/go/bin:/usr/local/go/bin/js_wasm:/usr/local/go/pkg/tool/js_wasm",
+ },
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ shim := domShim{dom}
+ global.Set("profile", jsfunc.NonBlocking(interop.ProfileJS))
+ global.Set("install", jsfunc.Promise(shim.installFunc))
+ global.Set("spawnTerminal", jsfunc.NonBlocking(terminal.SpawnTerminal))
+
+ if err := setUpFS(shim); err != nil {
+ panic(err)
+ }
+
+ if err := dom.Start(); err != nil {
+ panic(err)
+ }
+ log.Print("DOM started")
+
select {}
}
+
+func setUpFS(shim domShim) error {
+ const dirPerm = 0700
+ mkdirMount := func(mountPath string, durability idb.TransactionDurability) error {
+ if err := os.MkdirAll(mountPath, dirPerm); err != nil {
+ return err
+ }
+ if err := overlayIndexedDB(mountPath, durability); err != nil {
+ return err
+ }
+ return nil
+ }
+
+ if err := mkdirMount("/bin", idb.DurabilityRelaxed); err != nil {
+ return err
+ }
+ if err := mkdirMount("/home/me", idb.DurabilityDefault); err != nil {
+ return err
+ }
+ if err := mkdirMount("/home/me/.cache", idb.DurabilityRelaxed); err != nil {
+ return err
+ }
+ if err := mkdirMount("/tmp", idb.DurabilityRelaxed); err != nil {
+ return err
+ }
+
+ if err := os.MkdirAll("/usr/local/go", dirPerm); err != nil {
+ return err
+ }
+ if err := overlayTarGzip("/usr/local/go", "wasm/go.tar.gz", []string{
+ "/usr/local/go/bin/js_wasm",
+ "/usr/local/go/pkg/tool/js_wasm",
+ }); err != nil {
+ return err
+ }
+
+ if err := shim.Install("editor"); err != nil {
+ return err
+ }
+ if err := shim.Install("sh"); err != nil {
+ return err
+ }
+ return nil
+}
+
+func overlayIndexedDB(mountPath string, durability idb.TransactionDurability) error {
+ idbFS, err := indexeddb.NewFS(context.Background(), mountPath, indexeddb.Options{
+ TransactionDurability: durability,
+ })
+ if err != nil {
+ return err
+ }
+ return fs.Overlay(mountPath, idbFS)
+}
+
+func overlayTarGzip(mountPath, downloadPath string, skipCacheDirs []string) error {
+ log.Debug("Downloading overlay .tar.gz FS: ", downloadPath)
+ u, err := url.Parse(downloadPath)
+ if err != nil {
+ return err
+ }
+ // only download from current server, not just any URL
+ resp, err := http.Get(u.Path) // nolint:bodyclose // Body is closed in OverlayTarGzip handler to keep this async
+ if err != nil {
+ return err
+ }
+ log.Debug("Download response received. Reading body...")
+
+ skipDirs := make(map[string]bool)
+ for _, d := range skipCacheDirs {
+ skipDirs[common.ResolvePath("/", d)] = true
+ }
+ maxFileBytes := datasize.Kibibytes(100).Bytes()
+ shouldCache := func(name string, info hackpadfs.FileInfo) bool {
+ return !skipDirs[path.Dir(name)] && info.Size() < maxFileBytes
+ }
+ return fs.OverlayTarGzip(mountPath, resp.Body, true, shouldCache)
+}
diff --git a/server/package.json b/server/package.json
index 764d179..7f400fb 100644
--- a/server/package.json
+++ b/server/package.json
@@ -20,7 +20,7 @@
},
"scripts": {
"start": "react-scripts start",
- "start-go": "cd .. && nodemon --signal SIGINT -e go -d 2 -x 'make static || exit 1'",
+ "start-go": "cd .. && nodemon --signal SIGINT -e go -d 2 -x 'make -j8 static || exit 1'",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
diff --git a/server/public/wasmWorker.js b/server/public/wasmWorker.js
new file mode 100644
index 0000000..1f1c47b
--- /dev/null
+++ b/server/public/wasmWorker.js
@@ -0,0 +1,16 @@
+"use strict";
+
+async function runWasm(params) {
+ self.importScripts("wasm/wasm_exec.js")
+ const go = new Go()
+ const result = await WebAssembly.instantiateStreaming(fetch(params.wasm), go.importObject)
+ await go.run(result.instance)
+ close()
+}
+
+const params = new URLSearchParams(self.location.search)
+const paramsObj = {}
+for (const [key, value] of params) {
+ paramsObj[key] = value
+}
+runWasm(paramsObj)
diff --git a/server/src/App.js b/server/src/App.js
index f6d24b0..d16856c 100644
--- a/server/src/App.js
+++ b/server/src/App.js
@@ -6,13 +6,12 @@ import "@fontsource/roboto";
import '@fortawesome/fontawesome-free/css/all.css';
import Compat from './Compat';
import Loading from './Loading';
-import { install, run, observeGoDownloadProgress } from './Hackpad';
+import { observeGoDownloadProgress } from './Hackpad';
import { newEditor } from './Editor';
import { newTerminal } from './Terminal';
function App() {
const [percentage, setPercentage] = React.useState(0);
- const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
observeGoDownloadProgress(setPercentage)
@@ -20,19 +19,11 @@ function App() {
newTerminal,
newEditor,
}
- Promise.all([ install('editor'), install('sh') ])
- .then(() => {
- run('editor', '--editor=editor')
- setLoading(false)
- })
- }, [setLoading, setPercentage])
+ }, [setPercentage])
return (
<>
- { loading ? <>
-