mirror of
https://github.com/taigrr/wasm-experiments
synced 2025-01-18 04:03:21 -08:00
343 lines
8.4 KiB
Go
343 lines
8.4 KiB
Go
package deployer
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/dave/jsgo/assets/std"
|
|
"github.com/dave/jsgo/server/servermsg"
|
|
"github.com/dave/jsgo/server/wasm/messages"
|
|
"github.com/dave/services/constor/constormsg"
|
|
"github.com/dave/wasmgo/cmd/cmdconfig"
|
|
"github.com/dave/wasmgo/config"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/pkg/browser"
|
|
)
|
|
|
|
const CLIENT_VERSION = "1.0.0"
|
|
|
|
func Start(cfg *cmdconfig.Config) error {
|
|
|
|
var debug io.Writer
|
|
if cfg.Verbose {
|
|
debug = os.Stdout
|
|
} else {
|
|
debug = ioutil.Discard
|
|
}
|
|
|
|
// create a temp dir
|
|
tempDir, err := ioutil.TempDir("", "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
fpath := filepath.Join(tempDir, "out.wasm")
|
|
|
|
sourceDir, err := runGoList(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := runGoBuild(cfg, fpath, debug); err != nil {
|
|
return err
|
|
}
|
|
|
|
binaryBytes, err := ioutil.ReadFile(fpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
binarySha := sha1.New()
|
|
if _, err := io.Copy(binarySha, bytes.NewBuffer(binaryBytes)); err != nil {
|
|
return err
|
|
}
|
|
|
|
files := map[messages.DeployFileType]messages.DeployFile{}
|
|
|
|
files[messages.DeployFileTypeWasm] = messages.DeployFile{
|
|
DeployFileKey: messages.DeployFileKey{
|
|
Type: messages.DeployFileTypeWasm,
|
|
Hash: fmt.Sprintf("%x", binarySha.Sum(nil)),
|
|
},
|
|
Contents: binaryBytes,
|
|
}
|
|
|
|
loaderBuf := &bytes.Buffer{}
|
|
loaderSha := sha1.New()
|
|
wasmUrl := fmt.Sprintf("%s://%s/%x.wasm", config.Protocol[config.Pkg], config.Host[config.Pkg], binarySha.Sum(nil))
|
|
loaderVars := struct{ Binary string }{
|
|
Binary: wasmUrl,
|
|
}
|
|
if err := loaderTemplateMin.Execute(io.MultiWriter(loaderBuf, loaderSha), loaderVars); err != nil {
|
|
return err
|
|
}
|
|
|
|
files[messages.DeployFileTypeLoader] = messages.DeployFile{
|
|
DeployFileKey: messages.DeployFileKey{
|
|
Type: messages.DeployFileTypeLoader,
|
|
Hash: fmt.Sprintf("%x", loaderSha.Sum(nil)),
|
|
},
|
|
Contents: loaderBuf.Bytes(),
|
|
}
|
|
|
|
indexBuf := &bytes.Buffer{}
|
|
indexSha := sha1.New()
|
|
loaderUrl := fmt.Sprintf("%s://%s/%x.js", config.Protocol[config.Pkg], config.Host[config.Pkg], loaderSha.Sum(nil))
|
|
indexVars := struct{ Script, Loader string }{
|
|
Script: fmt.Sprintf("%s://%s/wasm_exec.%s.js", config.Protocol[config.Pkg], config.Host[config.Pkg], std.Wasm[true]),
|
|
Loader: loaderUrl,
|
|
}
|
|
indexTemplate := defaultIndexTemplate
|
|
if cfg.Index != "" {
|
|
indexFilename := cfg.Index
|
|
if cfg.Path != "" {
|
|
indexFilename = filepath.Join(sourceDir, cfg.Index)
|
|
}
|
|
indexTemplateBytes, err := ioutil.ReadFile(indexFilename)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
if err == nil {
|
|
indexTemplate, err = template.New("main").Parse(string(indexTemplateBytes))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if err := indexTemplate.Execute(io.MultiWriter(indexBuf, indexSha), indexVars); err != nil {
|
|
return err
|
|
}
|
|
|
|
files[messages.DeployFileTypeIndex] = messages.DeployFile{
|
|
DeployFileKey: messages.DeployFileKey{
|
|
Type: messages.DeployFileTypeIndex,
|
|
Hash: fmt.Sprintf("%x", indexSha.Sum(nil)),
|
|
},
|
|
Contents: indexBuf.Bytes(),
|
|
}
|
|
|
|
indexUrl := fmt.Sprintf("%s://%s/%x", config.Protocol[config.Index], config.Host[config.Index], indexSha.Sum(nil))
|
|
|
|
message := messages.DeployQuery{
|
|
Version: CLIENT_VERSION,
|
|
Files: []messages.DeployFileKey{
|
|
files[messages.DeployFileTypeWasm].DeployFileKey,
|
|
files[messages.DeployFileTypeIndex].DeployFileKey,
|
|
files[messages.DeployFileTypeLoader].DeployFileKey,
|
|
},
|
|
}
|
|
|
|
fmt.Fprintln(debug, "Querying server...")
|
|
|
|
protocol := "wss"
|
|
if config.Protocol[config.Wasm] == "http" {
|
|
protocol = "ws"
|
|
}
|
|
conn, _, err := websocket.DefaultDialer.Dial(
|
|
fmt.Sprintf("%s://%s/_wasm/", protocol, config.Host[config.Wasm]),
|
|
http.Header{"Origin": []string{fmt.Sprintf("%s://%s/", config.Protocol[config.Wasm], config.Host[config.Wasm])}},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
messageBytes, messageType, err := messages.Marshal(message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := conn.WriteMessage(messageType, messageBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
var response messages.DeployQueryResponse
|
|
var done bool
|
|
for !done {
|
|
_, replyBytes, err := conn.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message, err := messages.Unmarshal(replyBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch message := message.(type) {
|
|
case messages.DeployQueryResponse:
|
|
response = message
|
|
done = true
|
|
case servermsg.Queueing:
|
|
// don't print
|
|
case servermsg.Error:
|
|
return errors.New(message.Message)
|
|
case messages.DeployClientVersionNotSupported:
|
|
return errors.New("this client version is not supported - try `go get -u github.com/dave/wasmgo`")
|
|
default:
|
|
// unexpected
|
|
fmt.Fprintf(debug, "Unexpected message from server: %#v\n", message)
|
|
}
|
|
}
|
|
|
|
if len(response.Required) > 0 {
|
|
|
|
fmt.Fprintf(debug, "Files required: %d.\n", len(response.Required))
|
|
fmt.Fprintln(debug, "Bundling required files...")
|
|
|
|
var required []messages.DeployFile
|
|
for _, k := range response.Required {
|
|
file := files[k.Type]
|
|
if file.Hash == "" {
|
|
return errors.New("server requested file not found")
|
|
}
|
|
required = append(required, file)
|
|
}
|
|
|
|
payload := messages.DeployPayload{Files: required}
|
|
payloadBytes, payloadType, err := messages.Marshal(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := conn.WriteMessage(payloadType, payloadBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(debug, "Sending payload: %dKB.\n", len(payloadBytes)/1024)
|
|
|
|
done = false
|
|
for !done {
|
|
_, replyBytes, err := conn.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message, err := messages.Unmarshal(replyBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch message := message.(type) {
|
|
case messages.DeployDone:
|
|
done = true
|
|
case servermsg.Queueing:
|
|
// don't print
|
|
case servermsg.Error:
|
|
return errors.New(message.Message)
|
|
case constormsg.Storing:
|
|
if message.Remain > 0 || message.Finished > 0 {
|
|
fmt.Fprintf(debug, "Storing, %d to go.\n", message.Remain)
|
|
}
|
|
default:
|
|
// unexpected
|
|
fmt.Fprintf(debug, "Unexpected message from server: %#v\n", message)
|
|
}
|
|
}
|
|
|
|
fmt.Fprintln(debug, "Sending done.")
|
|
|
|
} else {
|
|
fmt.Fprintln(debug, "No files required.")
|
|
}
|
|
|
|
outputVars := struct {
|
|
Page string
|
|
Loader string
|
|
}{
|
|
Page: indexUrl,
|
|
Loader: loaderUrl,
|
|
}
|
|
|
|
if cfg.Json {
|
|
out, err := json.Marshal(outputVars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(out))
|
|
} else {
|
|
tpl, err := template.New("main").Parse(cfg.Template)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tpl.Execute(os.Stdout, outputVars)
|
|
fmt.Println("")
|
|
}
|
|
|
|
if cfg.Open {
|
|
browser.OpenURL(indexUrl)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runGoBuild(cfg *cmdconfig.Config, fpath string, debug io.Writer) error {
|
|
args := []string{"build", "-o", fpath}
|
|
|
|
extraFlags := strings.Fields(cfg.Flags)
|
|
for _, f := range extraFlags {
|
|
args = append(args, f)
|
|
}
|
|
|
|
if cfg.BuildTags != "" {
|
|
args = append(args, "-tags", cfg.BuildTags)
|
|
}
|
|
|
|
path := "."
|
|
if cfg.Path != "" {
|
|
path = cfg.Path
|
|
}
|
|
args = append(args, path)
|
|
|
|
fmt.Fprintln(debug, "Compiling...")
|
|
|
|
cmd := exec.Command(cfg.Command, args...)
|
|
cmd.Env = os.Environ()
|
|
cmd.Env = append(cmd.Env, "GOARCH=wasm")
|
|
cmd.Env = append(cmd.Env, "GOOS=js")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
if strings.Contains(string(output), "unsupported GOOS/GOARCH pair js/wasm") {
|
|
return errors.New("you need Go v1.11 to compile WASM. It looks like your default `go` command is not v1.11. Perhaps you need the -c flag to specify a custom command name - e.g. `-c=go1.11beta3`")
|
|
}
|
|
return fmt.Errorf("%v: %s", err, string(output))
|
|
}
|
|
if len(output) > 0 {
|
|
return fmt.Errorf("%s", string(output))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runGoList(cfg *cmdconfig.Config) (string, error) {
|
|
args := []string{"list"}
|
|
|
|
if cfg.BuildTags != "" {
|
|
args = append(args, "-tags", cfg.BuildTags)
|
|
}
|
|
|
|
args = append(args, "-f", "{{.Dir}}")
|
|
|
|
path := "."
|
|
if cfg.Path != "" {
|
|
path = cfg.Path
|
|
}
|
|
args = append(args, path)
|
|
|
|
cmd := exec.Command(cfg.Command, args...)
|
|
cmd.Env = os.Environ()
|
|
cmd.Env = append(cmd.Env, "GOARCH=wasm")
|
|
cmd.Env = append(cmd.Env, "GOOS=js")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
if strings.Contains(string(output), "unsupported GOOS/GOARCH pair js/wasm") {
|
|
return "", errors.New("you need Go v1.11 to compile WASM. It looks like your default `go` command is not v1.11. Perhaps you need the -c flag to specify a custom command name - e.g. `-c=go1.11beta3`")
|
|
}
|
|
return "", fmt.Errorf("%v: %s", err, string(output))
|
|
}
|
|
return string(output), nil
|
|
}
|