├── LICENSE ├── README.md ├── build ├── client.js.go ├── index.html.go ├── package.go └── script.js.go ├── cmd ├── example │ └── main.go ├── golang-nw-build │ └── build.go └── golang-nw-pkg │ └── pkg.go ├── doc.go ├── nw.go └── pkg └── pkg.go /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | golang-nw 2 | ========= 3 | 4 | Call a golang web application from node-webkit to get a native looking application. 5 | 6 | See [the documentation](http://godoc.org/github.com/lonnc/golang-nw) for usage instructions. 7 | -------------------------------------------------------------------------------- /build/client.js.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | const client = ` 4 | "use strict"; 5 | 6 | exports.createClient = function(args) { 7 | var events = require('events'); 8 | var channel = new events.EventEmitter(); 9 | var http = require('http'); 10 | var server = http.createServer(function (request, response) { 11 | if (request.method!='POST') { 12 | response.writeHead(404); 13 | response.end(''); 14 | return; 15 | } 16 | 17 | var body = ''; 18 | request.on('data', function(chunk) { body += chunk.toString(); }); 19 | request.on('end', function() { 20 | switch (request.url) { 21 | case '/redirect': 22 | channel.emit('redirect', body); 23 | response.writeHead(204); 24 | response.end(''); 25 | break; 26 | case '/error': 27 | channel.emit('error', body); 28 | response.writeHead(204); 29 | response.end(''); 30 | break; 31 | default: 32 | response.writeHead(404); 33 | response.end(''); 34 | }; 35 | }); 36 | }); 37 | 38 | server.listen(0, '127.0.0.1', 1, function() { 39 | var nodeWebkitAddr = 'http://127.0.0.1:'+server.address().port; 40 | console.log('Listening for golang-nw on '+nodeWebkitAddr); 41 | startClient(channel, nodeWebkitAddr, args); 42 | }); 43 | 44 | return channel; 45 | }; 46 | 47 | function logMessage(data, logger) { 48 | var lines = data.toString().split('\n'); 49 | for (var i = 0; i < lines.length; i++) { 50 | if (lines[i]) { 51 | logger.call(console, lines[i]); 52 | } 53 | } 54 | } 55 | 56 | function startClient(channel, nodeWebkitAddr, args) { 57 | var path = require('path'); 58 | var exe = '.'+path.sep+'{{ .Bin }}'; 59 | console.log('Using client: ' + exe); 60 | 61 | // Make the exe executable 62 | var fs = require('fs'); 63 | fs.chmodSync(exe, '755'); 64 | 65 | // Now start the client process 66 | var childProcess = require('child_process'); 67 | 68 | var env = process.env; 69 | env['{{ .EnvVar }}'] = nodeWebkitAddr; 70 | var p = childProcess.spawn(exe, args, {env: env}); 71 | 72 | p.stdout.on('data', function(data) { 73 | logMessage(data, console.log); 74 | }); 75 | 76 | p.stderr.on('data', function(data) { 77 | logMessage(data, console.error); 78 | }); 79 | 80 | p.on('error', function(err) { 81 | console.error('child error: ' + err); 82 | channel.emit('error', err); 83 | }); 84 | 85 | p.on('close', function(code) { 86 | console.log('child process closed with code ' + code); 87 | channel.emit('close', code); 88 | }); 89 | 90 | p.on('exit', function(code) { 91 | console.log('child process exited with code ' + code); 92 | channel.emit('exit', code); 93 | }); 94 | 95 | channel.kill = function() { 96 | p.kill(); 97 | } 98 | }; 99 | ` 100 | -------------------------------------------------------------------------------- /build/index.html.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | const index = ` 4 | 5 | 6 | 7 | {{ .Name }} 8 | 19 | 20 | 21 |

22 |   
23 | 
24 | 
25 | `
26 | 


--------------------------------------------------------------------------------
/build/package.go:
--------------------------------------------------------------------------------
  1 | package build
  2 | 
  3 | import (
  4 | 	"archive/zip"
  5 | 	"encoding/json"
  6 | 	"github.com/lonnc/golang-nw"
  7 | 	"io"
  8 | 	"io/ioutil"
  9 | 	"os"
 10 | 	"path"
 11 | 	"path/filepath"
 12 | 	"strings"
 13 | 	"text/template"
 14 | )
 15 | 
 16 | type Package struct {
 17 | 	Name   string `json:"name"`
 18 | 	Main   string `json:"main"`
 19 | 	Window Window `json:"window"`
 20 | 	Bin    string `json:"-"`
 21 | 	EnvVar string `json:"-"`
 22 | }
 23 | 
 24 | type Window struct {
 25 | 	Title      string `json:"title,omitempty"`
 26 | 	Toolbar    bool   `json:"toolbar"`
 27 | 	Show       bool   `json:"show,omitempty"`
 28 | 	Position   string `json:"position,omitempty"`
 29 | 	Width      int    `json:"width,omitempty"`
 30 | 	Height     int    `json:"height,omitempty"`
 31 | 	Fullscreen bool   `json:"fullscreen"`
 32 | 	Frame      bool   `json:"frame"`
 33 | }
 34 | 
 35 | type Templates struct {
 36 | 	IndexHtml string
 37 | 	ClientJs  string
 38 | 	ScriptJs  string
 39 | }
 40 | 
 41 | var DefaultTemplates = Templates{IndexHtml: index, ClientJs: client, ScriptJs: script}
 42 | 
 43 | // CreateNW creates a node-webkit .nw file
 44 | func (p Package) CreateNW(zw *zip.Writer, templates Templates, myapp io.Reader, includes string) error {
 45 | 	// Add in a couple of package defaults
 46 | 	p.Main = "index.html"
 47 | 	p.EnvVar = nw.EnvVar
 48 | 
 49 | 	if w, err := zw.Create("package.json"); err != nil {
 50 | 		return err
 51 | 	} else {
 52 | 		if _, err := p.writeJsonTo(w); err != nil {
 53 | 			return err
 54 | 		}
 55 | 	}
 56 | 
 57 | 	filenameTemplates := map[string]string{
 58 | 		"index.html": templates.IndexHtml,
 59 | 		"client.js":  templates.ClientJs,
 60 | 		"script.js":  templates.ScriptJs}
 61 | 	for filename, str := range filenameTemplates {
 62 | 		if w, err := zw.Create(filename); err != nil {
 63 | 			return err
 64 | 		} else {
 65 | 			if t, err := template.New(filename).Parse(str); err != nil {
 66 | 				return err
 67 | 			} else {
 68 | 				if err := t.Execute(w, p); err != nil {
 69 | 					return err
 70 | 				}
 71 | 			}
 72 | 		}
 73 | 	}
 74 | 
 75 | 	if includes != "" {
 76 | 		if err := copyIncludes(zw, includes); err != nil {
 77 | 			return err
 78 | 		}
 79 | 	}
 80 | 
 81 | 	binHeader := zip.FileHeader{Name: p.Bin}
 82 | 	binHeader.SetMode(0755) // Make it executable
 83 | 	if w, err := zw.CreateHeader(&binHeader); err != nil {
 84 | 		return err
 85 | 	} else {
 86 | 		if _, err := io.Copy(w, myapp); err != nil {
 87 | 			return err
 88 | 		}
 89 | 	}
 90 | 
 91 | 	return nil
 92 | }
 93 | 
 94 | func (p Package) writeJsonTo(w io.Writer) (int64, error) {
 95 | 	b, err := json.Marshal(p)
 96 | 	if err != nil {
 97 | 		return 0, err
 98 | 	}
 99 | 	n, err := w.Write(b)
100 | 	return int64(n), err
101 | }
102 | 
103 | // Copy any files from the includes directory
104 | func copyIncludes(zw *zip.Writer, includes string) error {
105 | 	includes = path.Clean(includes)
106 | 	if !strings.HasSuffix(includes, "/") {
107 | 		includes += "/"
108 | 	}
109 | 	return filepath.Walk(includes, func(path string, info os.FileInfo, err error) error {
110 | 		if err != nil {
111 | 			return err
112 | 		}
113 | 		if info.IsDir() {
114 | 			return nil
115 | 		}
116 | 		path = filepath.ToSlash(path)
117 | 		if w, err := zw.Create(strings.TrimPrefix(path, includes)); err != nil {
118 | 			return err
119 | 		} else {
120 | 			b, err := ioutil.ReadFile(path)
121 | 			if err != nil {
122 | 				return err
123 | 			} else {
124 | 				_, err := w.Write(b)
125 | 				if err != nil {
126 | 					return err
127 | 				}
128 | 			}
129 | 		}
130 | 		return nil
131 | 	})
132 | }
133 | 


--------------------------------------------------------------------------------
/build/script.js.go:
--------------------------------------------------------------------------------
 1 | package build
 2 | 
 3 | const script = `
 4 | "use strict";
 5 | 
 6 | var gui = require('nw.gui');
 7 | var os = require('os');
 8 | var win = gui.Window.get();
 9 | 
10 | win.on('loaded', function() {
11 |     // Restore window location on startup.
12 |     if (window.localStorage.width && window.localStorage.height) {
13 |         win.resizeTo(parseInt(window.localStorage.width), parseInt(window.localStorage.height));
14 |         win.moveTo(parseInt(window.localStorage.x), parseInt(window.localStorage.y));
15 |     }
16 | 
17 |     // Ensure we are visible
18 |     win.show();
19 | 
20 |     // Start client
21 |     var client = require('./client.js');
22 | 
23 |     var msg = function(s) {
24 |         var state = document.getElementById('state');
25 |         state.appendChild(document.createTextNode(s + '\n'));
26 |     };
27 | 
28 |     var clientProcess = client.createClient(gui.App.argv);
29 |     clientProcess.
30 |     on('error', function(err) {
31 |         msg('Error: ' + err);
32 |     }).
33 |     on('redirect', function(url) {
34 |         window.location.href = url;
35 |     });
36 | 
37 |     // And kill client when we close the window
38 |     win.on('close', function() {
39 |         clientProcess.kill();
40 |     });
41 | });
42 | 
43 | // Save size on close.
44 | win.on('close', function() {
45 |     window.localStorage.x = win.x;
46 |     window.localStorage.y = win.y;
47 |     window.localStorage.width = win.width;
48 |     window.localStorage.height = win.height;
49 |     this.close(true);
50 | });
51 | 
52 | // Workaround for copy-paste working on Mac.
53 | if(os.platform() == "darwin") {
54 |      var nativeMenuBar = new gui.Menu({ type: "menubar" });
55 |      nativeMenuBar.createMacBuiltin("App Name");
56 |      win.menu = nativeMenuBar;
57 | }
58 | 
59 | `
60 | 


--------------------------------------------------------------------------------
/cmd/example/main.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"github.com/lonnc/golang-nw"
 6 | 	"net/http"
 7 | )
 8 | 
 9 | func main() {
10 | 	// Setup our handler
11 | 	http.HandleFunc("/", hello)
12 | 
13 | 	// Create a link back to node-webkit using the environment variable
14 | 	// populated by golang-nw's node-webkit code
15 | 	nodeWebkit, err := nw.New()
16 | 	if err != nil {
17 | 		panic(err)
18 | 	}
19 | 
20 | 	// Pick a random localhost port, start listening for http requests using default handler
21 | 	// and send a message back to node-webkit to redirect
22 | 	if err := nodeWebkit.ListenAndServe(nil); err != nil {
23 | 		panic(err)
24 | 	}
25 | }
26 | 
27 | func hello(w http.ResponseWriter, r *http.Request) {
28 | 	fmt.Fprintf(w, "Hello from golang.")
29 | }
30 | 


--------------------------------------------------------------------------------
/cmd/golang-nw-build/build.go:
--------------------------------------------------------------------------------
 1 | package main
 2 | 
 3 | import (
 4 | 	"archive/zip"
 5 | 	"flag"
 6 | 	"github.com/lonnc/golang-nw/build"
 7 | 	"os"
 8 | 	"path/filepath"
 9 | )
10 | 
11 | var (
12 | 	app         = "myapp.exe"
13 | 	out         = "myapp.nw"
14 | 	name        = "My Application"
15 | 	includesDir = ""
16 | )
17 | 
18 | func main() {
19 | 	flag.StringVar(&app, "app", app, "Application to be wrapped by node-webkit.")
20 | 	flag.StringVar(&name, "name", name, "Application name.")
21 | 	flag.StringVar(&out, "out", out, "Destination file for generated node-webkit .nw file.")
22 | 	flag.StringVar(&includesDir, "includesDir", includesDir, "Directory containing additional files to bundle with the .nw file")
23 | 	flag.Parse()
24 | 
25 | 	if err := nwBuild(); err != nil {
26 | 		panic(err)
27 | 	}
28 | }
29 | 
30 | func nwBuild() error {
31 | 	w, err := os.Create(out)
32 | 	if err != nil {
33 | 		return err
34 | 	}
35 | 	defer w.Close()
36 | 
37 | 	zw := zip.NewWriter(w)
38 | 	defer zw.Close()
39 | 
40 | 	r, err := os.Open(app)
41 | 	if err != nil {
42 | 		return err
43 | 	}
44 | 	defer r.Close()
45 | 
46 | 	bin := filepath.Base(app)
47 | 	p := build.Package{Name: name, Bin: bin, Window: build.Window{Title: name}}
48 | 
49 | 	if err := p.CreateNW(zw, build.DefaultTemplates, r, includesDir); err != nil {
50 | 		return err
51 | 	}
52 | 
53 | 	return nil
54 | }
55 | 


--------------------------------------------------------------------------------
/cmd/golang-nw-pkg/pkg.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"archive/zip"
  5 | 	"flag"
  6 | 	"fmt"
  7 | 	"github.com/lonnc/golang-nw/build"
  8 | 	"github.com/lonnc/golang-nw/pkg"
  9 | 	"os"
 10 | 	"path/filepath"
 11 | 	"runtime"
 12 | )
 13 | 
 14 | var (
 15 | 	app         = "myapp"
 16 | 	name        = "My Application"
 17 | 	bin         = "myapp"
 18 | 	binDir      = "."
 19 | 	cacheDir    = "."
 20 | 	nwVersion   = "v0.11.6"
 21 | 	nwOs        = runtime.GOOS
 22 | 	nwArch      = runtime.GOARCH
 23 | 	toolbar     = true
 24 | 	includesDir = ""
 25 | 	fullscreen  = false
 26 | 	frame       = true
 27 | )
 28 | 
 29 | func main() {
 30 | 	if runtime.GOOS == "windows" {
 31 | 		app = app + ".exe"
 32 | 		bin = bin + ".exe"
 33 | 	}
 34 | 
 35 | 	flag.StringVar(&app, "app", app, "Web application to be wrapped by node-webkit.")
 36 | 	flag.StringVar(&name, "name", name, "Application name.")
 37 | 	flag.StringVar(&bin, "bin", bin, "Destination file for combined application and node-webkit .nw file (will be placed in binDir directory).")
 38 | 	flag.StringVar(&binDir, "binDir", binDir, "Destination directory for bin and dependencies.")
 39 | 	flag.StringVar(&cacheDir, "cacheDir", cacheDir, "Directory to cache node-webkit download.")
 40 | 	flag.StringVar(&nwVersion, "version", nwVersion, "node-webkit version.")
 41 | 	flag.StringVar(&nwOs, "os", nwOs, "Target os [linux|windows|darwin].")
 42 | 	flag.StringVar(&nwArch, "arch", nwArch, "Target arch [386|amd64].")
 43 | 	flag.BoolVar(&toolbar, "toolbar", toolbar, "Enable toolbar.")
 44 | 	flag.StringVar(&includesDir, "includesDir", includesDir, "Directory containing additional files to bundle with the .nw file")
 45 | 	flag.BoolVar(&fullscreen, "fullscreen", fullscreen, "Enable fullscreen mode.")
 46 | 	flag.BoolVar(&frame, "frame", frame, "Set to false to make window frameless.")
 47 | 	flag.Parse()
 48 | 
 49 | 	p := pkg.New(nwVersion, nwOs, nwArch)
 50 | 
 51 | 	if filepath.Base(bin) != bin {
 52 | 		panic(fmt.Errorf("bin %q includes a path", bin))
 53 | 	}
 54 | 
 55 | 	nw := filepath.Join(binDir, bin+".nw")
 56 | 	fmt.Printf("Building:\t %s\n", nw)
 57 | 	if err := nwBuild(nw); err != nil {
 58 | 		panic(err)
 59 | 	}
 60 | 
 61 | 	fmt.Printf("Downloading:\t %s\n", p.Url)
 62 | 	// Ensure cache directory exists
 63 | 	if err := os.MkdirAll(cacheDir, 0755); err != nil {
 64 | 		panic(err)
 65 | 	}
 66 | 	nodeWebkitPath, err := nwDownload(p)
 67 | 	if err != nil {
 68 | 		panic(err)
 69 | 	}
 70 | 
 71 | 	out := filepath.Join(binDir, bin)
 72 | 	fmt.Printf("Packaging:\t %s\n", out)
 73 | 	// Ensure bin directory exists
 74 | 	if err := os.MkdirAll(binDir, 0755); err != nil {
 75 | 		panic(err)
 76 | 	}
 77 | 
 78 | 	if err := nwPkg(p, nodeWebkitPath, nw); err != nil {
 79 | 		panic(err)
 80 | 	}
 81 | }
 82 | 
 83 | func nwBuild(nw string) error {
 84 | 	w, err := os.Create(nw)
 85 | 	if err != nil {
 86 | 		return err
 87 | 	}
 88 | 	defer w.Close()
 89 | 
 90 | 	zw := zip.NewWriter(w)
 91 | 	defer zw.Close()
 92 | 
 93 | 	r, err := os.Open(app)
 94 | 	if err != nil {
 95 | 		return err
 96 | 	}
 97 | 	defer r.Close()
 98 | 
 99 | 	bin := filepath.Base(app)
100 | 	p := build.Package{Name: name, Bin: bin, Window: build.Window{Title: name, Toolbar: toolbar, Fullscreen: fullscreen, Frame: frame}}
101 | 
102 | 	if err := p.CreateNW(zw, build.DefaultTemplates, r, includesDir); err != nil {
103 | 		return err
104 | 	}
105 | 
106 | 	return nil
107 | }
108 | 
109 | func nwDownload(p pkg.Pkg) (string, error) {
110 | 	return p.Download(cacheDir)
111 | }
112 | 
113 | func nwPkg(p pkg.Pkg, nodeWebkitPath string, nw string) error {
114 | 	r, err := os.Open(nw)
115 | 	if err != nil {
116 | 		return err
117 | 	}
118 | 	defer r.Close()
119 | 
120 | 	if err := p.Package(nodeWebkitPath, r, bin, binDir); err != nil {
121 | 		return err
122 | 	}
123 | 
124 | 	return nil
125 | }
126 | 


--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
 1 | /*
 2 | Call a golang web application from node-webkit to get a native looking application.
 3 | 
 4 | 
 5 | Instructions
 6 | 
 7 | 
 8 | Go get golang-nw:
 9 | 
10 |     go get github.com/lonnc/golang-nw/cmd/golang-nw-pkg
11 | 
12 | Create an app:
13 | 
14 | See https://github.com/lonnc/golang-nw/blob/master/cmd/example/main.go
15 | 	package main
16 | 
17 | 	import (
18 | 		"fmt"
19 | 		"github.com/lonnc/golang-nw"
20 | 		"net/http"
21 | 	)
22 | 
23 | 	func main() {
24 | 		// Setup our handler
25 | 		http.HandleFunc("/", hello)
26 | 
27 | 		// Create a link back to node-webkit using the environment variable
28 | 		// populated by golang-nw's node-webkit code
29 | 		nodeWebkit, err := nw.New()
30 | 		if err != nil {
31 | 			panic(err)
32 | 		}
33 | 
34 | 		// Pick a random localhost port, start listening for http requests using default handler
35 | 		// and send a message back to node-webkit to redirect
36 | 		if err := nodeWebkit.ListenAndServe(nil); err != nil {
37 | 			panic(err)
38 | 		}
39 | 	}
40 | 
41 | 	func hello(w http.ResponseWriter, r *http.Request) {
42 | 		fmt.Fprintf(w, "Hello from golang.")
43 | 	}
44 | 
45 | 
46 | Build your app:
47 | 
48 |     go install .\src\github.com\lonnc\golang-nw\cmd\example
49 | 
50 | 
51 | Wrap it in node-webkit:
52 | 
53 |     .\bin\golang-nw-pkg.exe -app=.\bin\example.exe -name="My Application" -bin="myapp.exe" -toolbar=false
54 | 
55 |     Building:        myapp.exe.nw
56 |     Downloading:     https://s3.amazonaws.com/node-webkit/v0.8.4/node-webkit-v0.8.4-win-ia32.zip
57 |     Packaging:       myapp.exe
58 | 
59 | You are now good to go:
60 | 
61 |     .\myapp.exe
62 | 
63 | You may want to create your own build script so you can control window dimensions etc.
64 | See http://godoc.org/github.com/lonnc/golang-nw/build and
65 | https://github.com/lonnc/golang-nw/blob/master/cmd/golang-nw-pkg/pkg.go
66 | 
67 | Command line options:
68 | 
69 |     $ ./bin/golang-nw-pkg -h
70 |     Usage of ./bin/golang-nw-pkg:
71 |       -app="myapp": Web application to be wrapped by node-webkit.
72 |       -arch="amd64": Target arch [386|amd64].
73 |       -bin="myapp": Destination file for combined application and node-webkit .nw file (will be placed in binDir directory).
74 |       -binDir=".": Destination directory for bin and dependencies.
75 |       -cacheDir=".": Directory to cache node-webkit download.
76 |       -frame=true: Set to false to make window frameless.
77 |       -fullscreen=false: Enable fullscreen mode.
78 |       -includesDir="": Directory containing additional files to bundle with the .nw file
79 |       -name="My Application": Application name.
80 |       -os="linux": Target os [linux|windows|darwin].
81 |       -toolbar=true: Enable toolbar.
82 |       -version="v0.9.2": node-webkit version.
83 | 
84 | Known issues:
85 | 
86 | 1) libudev.so.0 - On ubuntu >=13.10 (and similar) libudev.so.0 has been removed.
87 | tl;dr:
88 | 
89 |     $ ./bin/golang-nw-pkg -app=./bin/example -name="My Application" -bin="myapp" -toolbar=false
90 |     $ sed -i -e 's/udev\.so\.0/udev.so.1/g' myapp
91 | 
92 | Node-webkit has various work arounds described at https://github.com/rogerwang/node-webkit/wiki/The-solution-of-lacking-libudev.so.0
93 | 
94 | 2) Download of node-webkit appears to stall - It's a ~43MB download and can take longer than expected (it could do with some feedback).
95 | 
96 | */
97 | package nw
98 | 


--------------------------------------------------------------------------------
/nw.go:
--------------------------------------------------------------------------------
 1 | package nw
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"fmt"
 6 | 	"net"
 7 | 	"net/http"
 8 | 	"os"
 9 | 	"strings"
10 | )
11 | 
12 | const EnvVar = "GOLANG-NW"
13 | 
14 | var ErrMissingEnvVariable = errors.New("missing environment variable '" + EnvVar + "'")
15 | 
16 | type NodeWebkit struct {
17 | 	Url string // URL to issue callback command to
18 | }
19 | 
20 | func New() (NodeWebkit, error) {
21 | 	url := os.Getenv(EnvVar)
22 | 	if url == "" {
23 | 		return NodeWebkit{}, ErrMissingEnvVariable
24 | 	}
25 | 	if !strings.HasSuffix(url, "/") {
26 | 		url = url + "/"
27 | 	}
28 | 
29 | 	return NodeWebkit{url}, nil
30 | }
31 | 
32 | // ListenAndServe listens to a random port on localhost
33 | // and the issues a Redirect to node-webkit.
34 | // If handler is nil then the http.DefaultHandler will be used
35 | func (n NodeWebkit) ListenAndServe(handler http.Handler) error {
36 | 	httpAddr := "127.0.0.1:0"
37 | 	listener, err := net.Listen("tcp", httpAddr)
38 | 	// If we were not able to establish a socket send error back to node-webkit
39 | 	if err != nil {
40 | 		// Send error message to node-webkit
41 | 		return n.Error(err.Error())
42 | 	}
43 | 	defer listener.Close()
44 | 
45 | 	errs := make(chan error)
46 | 	defer close(errs)
47 | 
48 | 	// Issue redirect asynchronously so we can get on with the business of serving http requests
49 | 	go func() {
50 | 		if err := n.Redirect("http://" + listener.Addr().String() + "/"); err != nil {
51 | 			errs <- err
52 | 		}
53 | 	}()
54 | 
55 | 	// Now start the normal http listener
56 | 	go func() {
57 | 		if err := http.Serve(listener, handler); err != nil {
58 | 			errs <- err
59 | 		}
60 | 	}()
61 | 
62 | 	// Wait for an error from either serving or the redirect
63 | 	if err := <-errs; err != nil {
64 | 		// and forward it over to node-webkit
65 | 		return n.Error(err.Error())
66 | 	}
67 | 	return nil
68 | }
69 | 
70 | // Redirect sends a redirect message to node-webkit
71 | func (n NodeWebkit) Redirect(url string) error {
72 | 	return n.send("redirect", url)
73 | }
74 | 
75 | // Redirect sends an error message to node-webkit
76 | func (n NodeWebkit) Error(msg string) error {
77 | 	return n.send("error", msg)
78 | }
79 | 
80 | func (n NodeWebkit) send(key string, value string) error {
81 | 	r, err := http.Post(n.Url+key, "text/plain", strings.NewReader(value))
82 | 	if err != nil {
83 | 		return err
84 | 	}
85 | 	if r.StatusCode < 200 || r.StatusCode > 299 {
86 | 		return fmt.Errorf("Unexpected status code: %d", r.StatusCode)
87 | 	}
88 | 	return nil
89 | }
90 | 


--------------------------------------------------------------------------------
/pkg/pkg.go:
--------------------------------------------------------------------------------
  1 | package pkg
  2 | 
  3 | import (
  4 | 	"archive/tar"
  5 | 	"archive/zip"
  6 | 	"compress/gzip"
  7 | 	"fmt"
  8 | 	"io"
  9 | 	"io/ioutil"
 10 | 	"net/http"
 11 | 	"os"
 12 | 	"path"
 13 | 	"path/filepath"
 14 | 	"strings"
 15 | )
 16 | 
 17 | type Pkg struct {
 18 | 	Url          string
 19 | 	Bin          string
 20 | 	Dependencies []string
 21 | }
 22 | 
 23 | func New(version string, nwOs string, nwArch string) Pkg {
 24 | 	pkgOs, ok := pkgOss[nwOs]
 25 | 	if !ok {
 26 | 		panic(fmt.Errorf("Unsupported os %q", nwOs))
 27 | 	}
 28 | 
 29 | 	var arch string
 30 | 	switch nwArch {
 31 | 	case "386":
 32 | 		arch = "ia32"
 33 | 	case "amd64":
 34 | 		arch = "x64"
 35 | 	default:
 36 | 		panic(fmt.Errorf("Unsupported arch %q", nwArch))
 37 | 	}
 38 | 
 39 | 	url := fmt.Sprintf("http://dl.node-webkit.org/%s/node-webkit-%s-%s-%s%s", version, version, pkgOs.os, arch, pkgOs.ext)
 40 | 
 41 | 	pkg := Pkg{
 42 | 		Url:          url,
 43 | 		Bin:          pkgOs.bin,
 44 | 		Dependencies: pkgOs.deps,
 45 | 	}
 46 | 
 47 | 	return pkg
 48 | }
 49 | 
 50 | // Download retrieves the Url into the passed directory.
 51 | // If destDir=="" then TempDir is used.
 52 | // The file path is returned upon completion
 53 | func (p Pkg) Download(destDir string) (string, error) {
 54 | 	if destDir == "" {
 55 | 		destDir = os.TempDir()
 56 | 	}
 57 | 
 58 | 	// Where do we want to download to
 59 | 	out := filepath.Join(destDir, path.Base(p.Url))
 60 | 
 61 | 	// See if we already have it
 62 | 	if exists, err := isExists(out); err != nil {
 63 | 		return out, err
 64 | 	} else if exists {
 65 | 		return out, nil
 66 | 	}
 67 | 
 68 | 	// Download into memory then write to disk after
 69 | 	client := http.DefaultClient
 70 | 	r, err := client.Get(p.Url)
 71 | 	if err != nil {
 72 | 		return "", err
 73 | 	}
 74 | 	defer r.Body.Close()
 75 | 
 76 | 	if r.StatusCode != 200 {
 77 | 		return "", fmt.Errorf("Failed to download %q got: %s", p.Url, r.Status)
 78 | 	}
 79 | 
 80 | 	if content, err := ioutil.ReadAll(r.Body); err != nil {
 81 | 		return "", err
 82 | 	} else {
 83 | 		if err := ioutil.WriteFile(out, content, 0666); err != nil {
 84 | 			return "", err
 85 | 		}
 86 | 	}
 87 | 
 88 | 	return out, nil
 89 | }
 90 | 
 91 | // Package wraps populates destDir with the node-webkit depedencies and cat nw.exe [nw content] > binName
 92 | func (p Pkg) Package(nodeWebkitPath string, nw io.Reader, binName string, destDir string) error {
 93 | 	// Ensure destDir exists
 94 | 	if err := os.MkdirAll(destDir, 0755); err != nil {
 95 | 		return err
 96 | 	}
 97 | 
 98 | 	// Check we have a zip
 99 | 	if nodeWebkitPathZip, err := ensureZip(nodeWebkitPath); err != nil {
100 | 		return err
101 | 	} else {
102 | 		nodeWebkitPath = nodeWebkitPathZip
103 | 	}
104 | 
105 | 	// Extract dependencies from zip file
106 | 	zr, err := zip.OpenReader(nodeWebkitPath)
107 | 	if err != nil {
108 | 		return err
109 | 	}
110 | 	defer zr.Close()
111 | 
112 | 	// Get list of files in the zip archive, excluding the preceding directory
113 | 	zipFiles := map[string]*zip.File{}
114 | 	for _, f := range zr.File {
115 | 		zipFiles[path.Base(f.Name)] = f
116 | 	}
117 | 
118 | 	if p.Bin != "" {
119 | 		if bin, ok := zipFiles[p.Bin]; !ok {
120 | 			return fmt.Errorf("Failed to find %s in %s", p.Bin, nodeWebkitPath)
121 | 		} else {
122 | 			if err := p.copyBin(bin, nw, binName, destDir); err != nil {
123 | 				return err
124 | 			}
125 | 		}
126 | 	}
127 | 
128 | 	if err := p.copyDependencies(zipFiles, destDir); err != nil {
129 | 		return err
130 | 	}
131 | 
132 | 	return nil
133 | }
134 | 
135 | func (p Pkg) copyBin(bin *zip.File, nw io.Reader, binName string, destDir string) error {
136 | 	r, err := bin.Open()
137 | 	if err != nil {
138 | 		return err
139 | 	}
140 | 	defer r.Close()
141 | 
142 | 	filename := filepath.Join(destDir, binName)
143 | 	w, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
144 | 	if err != nil {
145 | 		return err
146 | 	}
147 | 	defer w.Close()
148 | 
149 | 	// Copy nw binary
150 | 	_, err = io.Copy(w, r)
151 | 	if err != nil {
152 | 		return err
153 | 	}
154 | 
155 | 	// Copy nw
156 | 	_, err = io.Copy(w, nw)
157 | 	if err != nil {
158 | 		return err
159 | 	}
160 | 
161 | 	return nil
162 | }
163 | 
164 | func (p Pkg) copyDependencies(zipFiles map[string]*zip.File, destDir string) error {
165 | 	// And extract the dependencies
166 | 	for _, dep := range p.Dependencies {
167 | 		filename := filepath.Join(destDir, dep)
168 | 
169 | 		// Only copy over if it doesn't already exist
170 | 		if exists, err := isExists(filename); err != nil {
171 | 			return err
172 | 		} else if exists {
173 | 			continue
174 | 		}
175 | 
176 | 		// And copy it over
177 | 		var r io.ReadCloser = nil
178 | 		if zipFile, ok := zipFiles[dep]; !ok {
179 | 			return fmt.Errorf("Failed to find %s", dep)
180 | 		} else {
181 | 			if f, err := zipFile.Open(); err != nil {
182 | 				return err
183 | 			} else {
184 | 				r = f
185 | 			}
186 | 		}
187 | 		defer r.Close()
188 | 
189 | 		w, err := os.Create(filename)
190 | 		if err != nil {
191 | 			return err
192 | 		}
193 | 		defer w.Close()
194 | 
195 | 		if _, err = io.Copy(w, r); err != nil {
196 | 			return err
197 | 		}
198 | 
199 | 	}
200 | 
201 | 	return nil
202 | }
203 | 
204 | type pkgOs struct {
205 | 	os   string
206 | 	bin  string
207 | 	deps []string
208 | 	ext  string
209 | }
210 | 
211 | var windows = pkgOs{
212 | 	os:   "win",
213 | 	bin:  "nw.exe",
214 | 	deps: []string{"ffmpegsumo.dll", "icudtl.dat", "libEGL.dll", "libGLESv2.dll", "nw.pak"},
215 | 	ext:  ".zip",
216 | }
217 | 
218 | var linux = pkgOs{
219 | 	os:   "linux",
220 | 	bin:  "nw",
221 | 	deps: []string{"libffmpegsumo.so", "nw.pak", "icudtl.dat"},
222 | 	ext:  ".tar.gz",
223 | }
224 | 
225 | var darwin = pkgOs{
226 | 	os:   "osx",
227 | 	bin:  "",
228 | 	deps: []string{},
229 | 	ext:  ".zip",
230 | }
231 | 
232 | var pkgOss = map[string]pkgOs{
233 | 	"windows": windows,
234 | 	"linux":   linux,
235 | 	"darwin":  darwin,
236 | }
237 | 
238 | func isExists(path string) (bool, error) {
239 | 	if _, err := os.Stat(path); err != nil {
240 | 		if os.IsNotExist(err) {
241 | 			return false, nil
242 | 		}
243 | 		return false, err
244 | 	}
245 | 	return true, nil
246 | }
247 | 
248 | // Check whether this is a .zip and return if not create a zip file and return that
249 | func ensureZip(filename string) (string, error) {
250 | 	if strings.HasSuffix(filename, ".zip") {
251 | 		return filename, nil
252 | 	}
253 | 
254 | 	if strings.HasSuffix(filename, ".tar.gz") {
255 | 		filenameZip := filename[:len(filename)-len(".tar.gz")] + ".zip"
256 | 		if exists, err := isExists(filenameZip); err != nil {
257 | 			return "", err
258 | 		} else {
259 | 			if !exists {
260 | 				if err := toZip(filename, filenameZip); err != nil {
261 | 					return "", err
262 | 				}
263 | 			}
264 | 			return filenameZip, err
265 | 		}
266 | 	}
267 | 
268 | 	return "", fmt.Errorf("Do not know how to create a zip archive from %s", filename)
269 | }
270 | 
271 | // convert a .tar.gz into a .zip
272 | func toZip(filenameTarGz string, filenameZip string) error {
273 | 	r, err := os.Open(filenameTarGz)
274 | 	if err != nil {
275 | 		return err
276 | 	}
277 | 	defer r.Close()
278 | 
279 | 	gz, err := gzip.NewReader(r)
280 | 	if err != nil {
281 | 		return err
282 | 	}
283 | 
284 | 	tgz := tar.NewReader(gz)
285 | 
286 | 	filenameZipTmp := filenameZip + ".tmp"
287 | 	w, err := os.Create(filenameZipTmp)
288 | 	if err != nil {
289 | 		return err
290 | 	}
291 | 	defer func() {
292 | 		if w != nil {
293 | 			w.Close()
294 | 		}
295 | 	}()
296 | 
297 | 	z := zip.NewWriter(w)
298 | 	defer func() {
299 | 		if z != nil {
300 | 			z.Close()
301 | 		}
302 | 	}()
303 | 
304 | 	for {
305 | 		hdr, err := tgz.Next()
306 | 		if err == io.EOF {
307 | 			break
308 | 		}
309 | 		if err != nil {
310 | 			return err
311 | 		}
312 | 		f, err := z.Create(hdr.Name)
313 | 		if err != nil {
314 | 			return err
315 | 		}
316 | 		if _, err := io.Copy(f, tgz); err != nil {
317 | 			return err
318 | 		}
319 | 	}
320 | 
321 | 	// Ok all done, lets close
322 | 	if err := z.Close(); err != nil {
323 | 		return err
324 | 	}
325 | 	if err := w.Close(); err != nil {
326 | 		return err
327 | 	}
328 | 	// And rename
329 | 	if err := os.Rename(filenameZipTmp, filenameZip); err != nil {
330 | 		return err
331 | 	}
332 | 	return nil
333 | }
334 | 


--------------------------------------------------------------------------------