├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── gulpfile.coffee ├── gulpfile.js ├── index.js ├── package.json ├── server.js ├── src ├── client.coffee ├── config.coffee ├── downloader.coffee ├── pubsub.js ├── server.coffee ├── snapshot.coffee └── utils.coffee └── test ├── communication.coffee ├── config.coffee ├── downloader.coffee ├── fixtures ├── app.zip └── snapshot.js ├── nwsnapshot.coffee ├── setup.coffee └── snapshot.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | tmp 4 | lib 5 | v8.log 6 | *.DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | node_modules 3 | bin 4 | tmp 5 | src 6 | v8.log 7 | *.DS_Store 8 | .gulpfile.coffee 9 | .gulpfile.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2014. Mikkel Schmidt. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 9 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 11 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 12 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 13 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 14 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Latest version: 0.3.1 2 | 3 | ## NOTE: Upgrading from 0.1.x to 0.2.x 4 | The default ports have changed to make room for x64 on osx and windows. See [defaults](#defaults) 5 | 6 | ## New in 0.3.1 7 | Support tested up to nwjs v0.25.3 8 | 9 | ## Introduction 10 | 11 | node-nw-snapshot is a cross platform buildserver and client for compiling and testing v8 snapshots of [node-webkit](github.com/rogerwang/node-webkit) code. It's simple to get up and running, if you already have virtual or local machine's running the needed operating systems. It will compile snapshots for any node-webkit version above v0.4.2, and automatically download the specified version for compilation and testing - no more manual fixing of 3 seperate vm's when upgrading your app to a new node-webkit version. 12 | 13 | Best of all, **no more broken snapshots** when deploying a new version of your app. 14 | 15 | ## Why does this exist? 16 | 17 | My application [Circadio](https://getcircadio.com/) has an autoupdater that will automatically update the application when i publish a new version. I need to protect my source code, so i use snapshots. I distribute Circadio for Windows, OS X and Linux which means i need some kind of server running on all the platforms for generating the snapshots for "one button deploys". Until now, that server has been extremely minimal and basically just consisted of an exec call and a simple http server. 18 | 19 | I started noticing that on almost every deploy, one of my distributions would fail. After looking over my buildscripts, application code and distribution server i couldn't find the source of the problem. What worried me even more, was that on 4 consecutive deploys of the same code, the distribution that failed to run was totally random. The first time it would be the Windows distribution, the next it would be the linux32 distribution, and so on. Sometimes everything just worked. 20 | 21 | I finally traced the source of the problem to the snapshot. It seems that nwsnapshot will sometimes fail silently and generate a snapshot that will [cause node-webkit to crash on launch](https://github.com/rogerwang/node-webkit/issues/1295). With no way to prevent this from happening, i set out to create node-nw-snapshot. 22 | 23 | ## How it works 24 | 25 | When the server receives a build command, it will download the specified node-webkit version as needed and inject a small test function (9 LOC) into your snapshot code. The package.json file of your app will be modified to use the generated snapshot. After that, the app will be launched with an automatically generated ID and the function will test for this id, before executing a callback to the snapshot server. 26 | 27 | Since the function is located inside the snapshotted code, the app won't make a request to the server if the snapshot is broken. That means you can be 100% certain that the snapshot you get back will run. 28 | 29 | Note: 3 things can happen when launching the app. 30 | 31 | * It will work and make a request to the server (yay!) 32 | * It will immediately crash 33 | * It will hang forever. 34 | 35 | In the last case there's a timeout of 10s before the process is terminated and the snapshot is deemed broken. 36 | 37 | ## Security 38 | 39 | Since the code you will be snapshotting is probably propriatary (if not, why snapshot?) you probably don't want to pass your code across the web. node-nw-snapshot uses insecure sockets (based on [Axon](github.com/visionmedia/axon)) to communicate between clients and servers, and your code will be transferred in plain text (for now). You will probably want to keep your servers on the local network (i use VirtualBox). Besides this concern there is no reason your servers couldn't be located remotely. 40 | 41 | ## Installation 42 | 43 | ```bash 44 | npm install nw-snapshot 45 | ``` 46 | 47 | ## Usage 48 | 49 | Server: 50 | 51 | ```bash 52 | npm start nw-snapshot 53 | ``` 54 | 55 | Client: 56 | 57 | in your buildscript: 58 | ```js 59 | SnapshotClient = require('nw-snapshot').Client; 60 | var client = new SnapshotClient("0.9.2", appSource, snapshotSource); 61 | // Connect to tcp://127.0.0.1:3001 62 | client.connect(3001, function(){ 63 | client.on('done', function(snapshot){ 64 | require('fs').writeFileSync(require('path').join(__dirname, 'snapshot.bin'), snapshot); 65 | console.log("Done compiling snapshot."); 66 | client.disconnect(); 67 | }); 68 | client.on('progress', function(err, iteration) { 69 | // Will run each time an iteration has failed. 70 | console.log("Iteration #" + iteration + " failed: " + err); 71 | }); 72 | client.on('fail', function(err, tries){ 73 | console.log("Failed to compile snapshot. Tried " + tries + " times."); 74 | }); 75 | // Run a maximum of 5 iterations. 76 | client.build(5); 77 | }); 78 | 79 | ``` 80 | appSource should be either a `Buffer` of, or the path to, your application zip (app.nw) without the code for the snapshot. 81 | snapshotSource is the js file that you want to compile into a snapshot. 82 | 83 | In your app's main .html file insert this snippet at the bottom of ``: 84 | ```html 85 | 88 | ``` 89 | 90 | #### Testing 91 | 92 | Start by cloning the repository: 93 | ```bash 94 | git clone https://github.com/miklschmidt/node-nw-snapshot.git 95 | cd node-nw-snapshot 96 | npm install -g gulp 97 | npm install 98 | ``` 99 | 100 | The tests use a minimally modified version of the [frameless-window](https://github.com/zcbenz/nw-sample-apps/tree/master/frameless-window) example from the official node-webkit example applications. 101 | 102 | ```bash 103 | gulp test 104 | ``` 105 | 106 | Want to test the ratio at which nwsnapshot will fail? 107 | ``` 108 | gulp test-nwsnapshot 109 | ``` 110 | 111 | On OSX with v0.8.1, nwsnapshot will produce a broken snapshot ~40 out of 100 runs. 112 | 113 | #### Defaults 114 | 115 | ##### Server socket ports: 116 | 117 | * osx32 servers will use 3001 118 | * osx64 servers will use 3002 119 | * win32 servers will use 3011 120 | * win64 servers will use 3012 121 | * linux32 servers will use 3021 122 | * linux64 servers will use 3022 123 | 124 | ##### Server http ports: 125 | 126 | * osx32 servers will use 3301 127 | * osx64 servers will use 3302 128 | * win32 servers will use 3311 129 | * win64 servers will use 3312 130 | * linux32 servers will use 3321 131 | * linux64 servers will use 3322 132 | 133 | ###### Configuration 134 | 135 | If you're starting the server with `node server.js` or similar you can provide commandline arguments to override the http port, the socket port, and the platform architecture to compile for. 136 | ``` 137 | node server.js --arch ia32 --httpport 1234 --sockport 4321 138 | node server.js --arch x64 139 | node server.js --sockport 4321 140 | ``` 141 | 142 | If you're using `npm start` to start the server, you can override the default socket port by doing: 143 | ``` 144 | npm config set nw-snapshot:sockport 1234 145 | ``` 146 | and the http port: 147 | ``` 148 | npm config set nw-snapshot:httpport 4321 149 | ``` 150 | and the platform architecture to compile for: 151 | ``` 152 | npm config set nw-snapshot:arch ia32 153 | ``` 154 | 155 | ##### Snapshot: 156 | ``` 157 | timeout: 10000ms # Time to wait before killing the node-webkit process and fail/try again 158 | ``` 159 | 160 | ## Cool stuff 161 | 162 | node-nw-snapshot comes with a downloader for downloading and extracting a specific version of node-webkit. You can use this class in your buildscript for automatically running your app in the version of you choosing. Here's an example using [gulp](https://github.com/gulpjs/gulp): 163 | 164 | ```javascript 165 | var exec = require('child_process').exec; 166 | var NodeWebkitDownloader = require('nw-snapshot').Downloader; 167 | var gulp = require('gulp'); 168 | var gutil = require('gulp-util'); 169 | 170 | gulp.task('run', ['insert-name-of-compile-task-here'], function(callback){ 171 | 172 | var version = gutil.env.nw || '0.9.2'; 173 | downloader = new NodeWebkitDownloader(version); 174 | downloader.ensure() 175 | .done(function(snapshotBin, nwBin){ 176 | appProcess = exec(nwBin + " " + path_to_you_app_folder); 177 | appProcess.stdout.pipe(process.stdout); 178 | appProcess.stderr.pipe(process.stderr); 179 | appProcess.on('exit', callback); 180 | }).fail(callback); 181 | 182 | }); 183 | ``` 184 | 185 | Now from the command line you can do: 186 | 187 | ```bash 188 | gulp run --nw 0.8.2 189 | ``` 190 | 191 | And magic will happen! 192 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | coffee = require 'gulp-coffee' 3 | exec = require('child_process').exec 4 | path = require 'path' 5 | mocha = require 'gulp-mocha' 6 | 7 | gulp.task 'compile', (done) -> 8 | gulp.src './src/*.coffee' 9 | .pipe coffee() 10 | .pipe gulp.dest './lib' 11 | .on 'end', () -> 12 | gulp.src './src/*.js' 13 | .pipe gulp.dest './lib' 14 | .on 'end', done 15 | null 16 | 17 | gulp.task 'test', ['compile'], -> 18 | gulp.src './test/setup.coffee' 19 | .pipe mocha reporter: 'spec' 20 | 21 | gulp.task 'prepublish', ['compile'], -> 22 | 23 | gulp.task 'test-nwsnapshot', ['compile'], -> 24 | gulp.src './test/nwsnapshot.coffee' 25 | .pipe mocha reporter: 'spec' -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('coffee-script/register'); 2 | require('./gulpfile.coffee'); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.Server = require("./lib/server"); 2 | module.exports.Client = require("./lib/client"); 3 | module.exports.Snapshot = require("./lib/snapshot"); 4 | module.exports.Downloader = require("./lib/downloader"); 5 | module.exports.PubSubSocket = require("./lib/pubsub"); 6 | module.exports.Utils = require("./lib/utils"); 7 | module.exports.Config = require("./lib/config"); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nw-snapshot", 3 | "version": "0.3.2", 4 | "description": "Tools for compiling and testing v8 snapshots of node-webkit applications", 5 | "main": "index", 6 | "keywords": ["node-webkit", "nw", "nwsnapshot", "snapshot", "build", "test", "server", "client"], 7 | "author": { 8 | "name": "Mikkel Schmidt", 9 | "email": "mikkel.schmidt@gmail.com" 10 | }, 11 | "directories": { 12 | "lib": "lib" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "http://github.com/miklschmidt/node-nw-snapshot.git" 17 | }, 18 | "bugs": "http://github.com/miklschmidt/node-nw-snapshot/issues", 19 | "license": "MIT", 20 | "scripts": { 21 | "start": "node server.js", 22 | "prepublish": "gulp prepublish" 23 | }, 24 | "config": { 25 | "sockport": false, 26 | "httpport": false, 27 | "arch": false 28 | }, 29 | "dependencies": { 30 | "axon": "~1.0.0", 31 | "debug": "~0.7.4", 32 | "decompress-zip": "^0.3.0", 33 | "escape-regexp": "0.0.1", 34 | "express": "~3.4.8", 35 | "glob": "^3.2.9", 36 | "gulp": "^3.9.1", 37 | "gulp-coffee": "^2.3.4", 38 | "gulp-mocha": "^3.0.1", 39 | "gulp-util": "^3.0.8", 40 | "jquery-deferred": "~0.3.0", 41 | "minimist": "^1.1.0", 42 | "mkdirp": "^0.5.1", 43 | "request": "~2.33.0", 44 | "rimraf": "^2.6.2", 45 | "tar": "^4.0.1" 46 | }, 47 | "devDependencies": { 48 | "coffee-script": "~1.7.0", 49 | "mocha": "~1.17.1", 50 | "should": "~3.1.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var nwsnapshot = require('./index.js'); 2 | console.log('Starting socket on port ' + nwsnapshot.Config.sockPort) 3 | console.log('Starting http server on port ' + nwsnapshot.Config.httpPort) 4 | nwsnapshot.Server.start() -------------------------------------------------------------------------------- /src/client.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Dependencies 3 | ### 4 | 5 | Emitter = require('events').EventEmitter 6 | fs = require 'fs' 7 | PubSubSocket = require './pubsub.js' 8 | 9 | 10 | ### 11 | # SnapshotClient class definition 12 | ### 13 | 14 | module.exports = class SnapshotClient extends Emitter 15 | 16 | ### 17 | # Constructs the class. 18 | # 19 | # @param {string} nwVersion 20 | # @param {(string|Buffer)} appSource 21 | # @param {(string|Buffer)} snapshotSource 22 | # @returns {SnapshotClient} 23 | # @api private 24 | ### 25 | constructor: (nwVersion, appSource, snapshotSource) -> 26 | throw new Error("missing nwVersion parameter") unless nwVersion 27 | throw new Error("missing appSource parameter") unless appSource 28 | throw new Error("missing snapshotSource parameter") unless snapshotSource 29 | 30 | @nwVersion = nwVersion 31 | 32 | if typeof appSource is 'string' and fs.existsSync(appSource) 33 | @appSource = fs.readFileSync(appSource) 34 | else if appSource instanceof Buffer 35 | @appSource = appSource 36 | else 37 | throw new Error('appSource parameter should be a buffer or a valid (existing) filepath.') 38 | 39 | if typeof snapshotSource is 'string' and fs.existsSync(snapshotSource) 40 | @snapshotSource = fs.readFileSync(snapshotSource) 41 | else if snapshotSource instanceof Buffer 42 | @snapshotSource = snapshotSource 43 | else 44 | throw new Error('snapshotSource parameter should be a buffer or a valid (existing) filepath.') 45 | 46 | @connected = false 47 | 48 | ### 49 | # Connects to the build server 50 | # 51 | # @param {(string|Number)} - If only a number is given address is assumed to be a port on localhost. 52 | # @param {Function} callback - called when connected 53 | # @returns {void} 54 | # @api public 55 | ### 56 | connect: (address, callback) -> 57 | unless typeof address is 'string' 58 | address = 'tcp://127.0.0.1:' + address 59 | @socket = new PubSubSocket() 60 | @socket.connect(address) 61 | @socket.on 'connect', () => 62 | @connected = true 63 | callback?() 64 | @socket.on 'message', () => 65 | if arguments[0].toString() is 'done' 66 | @emit 'done', arguments[1], arguments[2].toString() 67 | else 68 | @emit arguments[0].toString(), arguments[1].toString(), arguments[2].toString() 69 | @socket.on 'close', () => 70 | @connected = false 71 | 72 | ### 73 | # Tells the buildserver to start compiling the snapshot, and disconnects when it's done. 74 | # 75 | # @param {Number} iterations - how many times to try compiling before giving up. 76 | # @returns {void} 77 | # @api public 78 | ### 79 | build: (iterations) -> 80 | throw new Error("Not connected to build server") unless @connected 81 | @socket.send @nwVersion, @appSource, @snapshotSource, iterations + '' # axon breaks on numbers, 'cause they have no .length 82 | @socket.on 'message', (type) => 83 | if type in ['done', 'fail'] 84 | # Disconnect 85 | @disconnect() 86 | 87 | 88 | ### 89 | # Disconnects from the buildserver. 90 | # 91 | # @returns {void} 92 | # @api public 93 | ### 94 | disconnect: () -> 95 | @socket.removeAllListeners() 96 | @removeAllListeners() 97 | @socket.close() 98 | for sock in @socket.socks 99 | sock.end() 100 | sock.destroy() 101 | sock.unref() 102 | @connected = false 103 | -------------------------------------------------------------------------------- /src/config.coffee: -------------------------------------------------------------------------------- 1 | parseArgs = require 'minimist' 2 | 3 | config = {} 4 | config.hostIP = '0.0.0.0' 5 | config.arch = process.arch 6 | 7 | args = parseArgs process.argv.slice(2) 8 | 9 | if process.platform.match(/^darwin/) 10 | config.platform = 'osx' 11 | portOffset = 0 12 | else if process.platform.match(/^win/) 13 | config.platform = 'win' 14 | portOffset = 10 15 | else 16 | config.platform = 'linux' 17 | portOffset = 20 18 | 19 | # Platform architecture 20 | if args.arch 21 | config.arch = args.arch 22 | console.log "Using arch from CLI args: #{config.arch}" 23 | else if process.env.npm_package_config_arch and process.env.npm_package_config_arch isnt 'false' 24 | config.arch = process.env.npm_package_config_arch 25 | console.log "Using arch from npm package configuration: #{config.arch}" 26 | else 27 | config.arch = process.arch 28 | console.log "Defaulting to process.arch: #{config.arch}" 29 | 30 | if config.arch not in ['ia32', 'x64'] 31 | throw new Error("Unsupported platform architecture '#{config.arch}") 32 | else 33 | # Socket port 34 | if args.sockport 35 | config.sockPort = args.sockport 36 | console.log "Using socket port from CLI args: #{config.sockPort}" 37 | else if process.env.npm_package_config_sockport and process.env.npm_package_config_sockport isnt 'false' 38 | config.sockPort = process.env.npm_package_config_sockport 39 | console.log "Using socket port from npm package configuration: #{config.sockPort}" 40 | else 41 | if config.arch is 'ia32' 42 | config.sockPort = 3001 + portOffset 43 | else if config.arch is 'x64' 44 | config.sockPort = 3002 + portOffset 45 | console.log "Defaulting socket port to #{config.sockPort}" 46 | # Http port 47 | if args.httpport 48 | config.httpPort = args.httpport 49 | console.log "Using socket port from CLI args: #{config.httpPort}" 50 | else if process.env.npm_package_config_httpport and process.env.npm_package_config_httpport isnt 'false' 51 | config.httpPort = process.env.npm_package_config_httpport 52 | console.log "Using socket port from npm package configuration: #{config.httpPort}" 53 | else 54 | if config.arch is 'ia32' 55 | config.httpPort = 3301 + portOffset 56 | else if config.arch is 'x64' 57 | config.httpPort = 3302 + portOffset 58 | console.log "Defaulting http port to #{config.httpPort}" 59 | 60 | 61 | config.timeout = 10000 # ms before giving up and failing the test 62 | config.callbackURL = "http://127.0.0.1:#{config.httpPort}/callback" 63 | 64 | config.oldDownloadURL = "https://s3.amazonaws.com/node-webkit" 65 | config.newDownloadURL = "http://dl.node-webkit.org" 66 | config.nwjsDownloadUrl = "http://dl.nwjs.io" 67 | 68 | module.exports = config -------------------------------------------------------------------------------- /src/downloader.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Dependencies 3 | ### 4 | 5 | Config = require './config' 6 | Utils = require './utils' 7 | path = require 'path' 8 | fs = require 'fs' 9 | dfd = require('jquery-deferred').Deferred 10 | rimraf = require 'rimraf' 11 | mkdirp = require 'mkdirp' 12 | request = require 'request' 13 | 14 | ### 15 | # NodeWebkitDownloader Class definition 16 | ### 17 | 18 | module.exports = class NodeWebkitDownloader 19 | 20 | binFolder: "bin" 21 | 22 | ### 23 | # Constructor for the class that configures which version, platform and 24 | # architecture to use. 25 | # You can omit the platform and arch arguments and the instance will just 26 | # default to the current platform and architecture. 27 | # 28 | # @param {Object} version 29 | # @param {String} [platform] 30 | # @param {String} [arch] 31 | # @return {NodeWebkitDownloader} 32 | # @api private 33 | ### 34 | constructor: (@version, @platform = Config.platform, @arch = Config.arch, @minimalBuild = false) -> 35 | throw new Error "No version specified" unless @version 36 | unless @platform in ['win', 'osx', 'linux'] 37 | throw new Error "Platform must be one of 'osx', 'linux' or 'win'" 38 | unless @arch in ['ia32', 'x64'] 39 | throw new Error "Arch must be one of 'ia32' or 'x64'" 40 | {major, minor} = @parseVersion() 41 | if major > 12 and @platform is 'osx' and @arch isnt 'x64' 42 | throw new Error "Only x64 is supported for osx from nw v0.13.x and onwards" 43 | 44 | if Config.platform is 'win' and @platform is 'linux' 45 | throw new Error "Extracting for linux on windows is unsupported at the moment. See: https://github.com/miklschmidt/node-nw-snapshot/issues/3" 46 | 47 | ### 48 | # Returns the remote URL for the node-webkit archive. 49 | # 50 | # @return {String} 51 | # @api public 52 | ### 53 | getDownloadURL: () -> 54 | extension = if @platform is 'linux' then 'tar.gz' else 'zip' 55 | 56 | # Use old download url if version is < 0.8.0 57 | {major, minor} = @parseVersion() 58 | if major is 0 and minor < 8 59 | return "#{Config.oldDownloadURL}/v#{@version}/node-webkit-v#{@version}-#{@platform}-#{@arch}.#{extension}" 60 | else if major is 0 and minor < 12 61 | return "#{Config.newDownloadURL}/v#{@version}/node-webkit-v#{@version}-#{@platform}-#{@arch}.#{extension}" 62 | else 63 | if major is 0 and minor < 13 64 | return "#{Config.nwjsDownloadUrl}/v#{@version}/nwjs-v#{@version}-#{@platform}-#{@arch}.#{extension}" 65 | else 66 | if @minimalBuild 67 | return "#{Config.nwjsDownloadUrl}/v#{@version}/nwjs-v#{@version}-#{@platform}-#{@arch}.#{extension}" 68 | else 69 | return "#{Config.nwjsDownloadUrl}/v#{@version}/nwjs-sdk-v#{@version}-#{@platform}-#{@arch}.#{extension}" 70 | 71 | 72 | ### 73 | # Returns the local path to the directory where the node-webkit 74 | # distribution resides (or will be extracted to). 75 | # 76 | # @return {String} 77 | # @api public 78 | ### 79 | getLocalPath: () -> 80 | if @minimalBuild 81 | path.join __dirname, '..', @binFolder, @version, "#{@platform}-#{@arch}-minimal" 82 | else 83 | path.join __dirname, '..', @binFolder, @version, "#{@platform}-#{@arch}" 84 | 85 | ### 86 | # Returns the subfolder of the zip where the files are located, if it exists. 87 | # In 0.10.0 node-webkit switched their win and osx zip structure to match the linux ones. 88 | # 89 | # @return {String} 90 | # @api public 91 | ### 92 | getZipSubFolder: () -> 93 | # Split on '-' to get rid of "rc" part if present. 94 | {major, minor} = @parseVersion() 95 | if @platform is 'linux' or (major is 0 and minor > 9) or (major > 0) 96 | if @isNwjs() 97 | if @minimalBuild 98 | return "nwjs-v#{@version}-#{@platform}-#{@arch}" 99 | else 100 | return "nwjs-sdk-v#{@version}-#{@platform}-#{@arch}" 101 | else 102 | "node-webkit-v#{@version}-#{@platform}-#{@arch}" 103 | else 104 | "" 105 | 106 | parseVersion: () -> 107 | # Split on '-' to get rid of "rc" part if present. 108 | frags = @version.split('-')[0].split('.') 109 | major = parseInt frags[0] 110 | minor = parseInt frags[1] 111 | patch = parseInt frags[2] 112 | return {major, minor, patch} 113 | 114 | isNwjs: () -> 115 | {major, minor} = @parseVersion() 116 | (major is 0 and minor >= 12) or major > 0 117 | 118 | ### 119 | # Returns the local path to the snapshot binary. 120 | # NOTE: The binary might not exist, see verifyBinaries(). 121 | # 122 | # @return {String} 123 | # @api public 124 | ### 125 | getSnapshotBin: () -> 126 | snapshotExecutable = if @isNwjs() 127 | if @platform is 'win' 128 | 'nwjc.exe' 129 | else 130 | 'nwjc' 131 | else 132 | if @platform is 'win' 133 | 'nwsnapshot.exe' 134 | else 135 | 'nwsnapshot' 136 | 137 | 138 | folder = @getZipSubFolder() 139 | path.join @getLocalPath(), folder, snapshotExecutable 140 | 141 | ### 142 | # Returns the local path to the node-webkit binary. 143 | # NOTE: The binary might not exist, see verifyBinaries(). 144 | # 145 | # @return {String} 146 | # @api public 147 | ### 148 | getNwBin: () -> 149 | folder = @getZipSubFolder() 150 | switch @platform 151 | when 'win' 152 | path.join @getLocalPath(), folder, 'nw.exe' 153 | when 'osx' 154 | if @isNwjs() 155 | path.join @getLocalPath(), folder, 'nwjs.app', 'Contents', 'MacOS', 'nwjs' 156 | else 157 | path.join @getLocalPath(), folder, 'node-webkit.app', 'Contents', 'MacOS', 'node-webkit' 158 | when 'linux' 159 | path.join @getLocalPath(), folder, 'nw' 160 | 161 | ### 162 | # Downloads the node-webkit archive. 163 | # 164 | # @return {Promise} 165 | # @api private 166 | ### 167 | download: () -> 168 | downloadDeferred = dfd() 169 | url = @getDownloadURL() 170 | filename = url.split('/').slice(-1)[0] 171 | destinationFile = path.join @getLocalPath(), filename 172 | 173 | # Error handler 174 | handleError = (err) => downloadDeferred.rejectWith @, [err] 175 | 176 | # Create the directory 177 | if fs.existsSync(destinationFile) 178 | downloadDeferred.resolveWith @, [destinationFile] 179 | else 180 | mkdirp @getLocalPath(), (err) => 181 | handleError err if err 182 | 183 | destinationStream = fs.createWriteStream destinationFile 184 | 185 | # Start the request for the file 186 | reqObj = {url} 187 | reqObj.proxy = process.env.http_proxy if process.env.http_proxy? 188 | req = request reqObj 189 | 190 | # Error handling 191 | 192 | destinationStream.on 'error', handleError 193 | req.on 'error', handleError 194 | 195 | # Success handling 196 | destinationStream.on 'close', () => 197 | downloadDeferred.resolveWith @, [destinationFile] 198 | 199 | # We need to listen for the response event, since 404 responses don't seem to 200 | # trigger the error event. Without this check the promise would be resolved even 201 | # if the server responded with 404. 202 | req.on 'response', (response) => 203 | if response.statusCode isnt 200 204 | downloadDeferred.rejectWith @, [ 205 | new Error("Bad response (#{response.statusCode}). 206 | The version you requested (#{@version}) probably doesn't exist.") 207 | ] 208 | # Pipe the request data to the local file 209 | req.pipe destinationStream 210 | 211 | downloadDeferred.promise() 212 | 213 | ### 214 | # Deletes the directory where the node-webkit distribution resides. 215 | # 216 | # @return {Promise} 217 | # @api public 218 | ### 219 | cleanVersionDirectoryForPlatform: () -> 220 | cleanDeferred = dfd() 221 | # Delete the directory and all its content. 222 | rimraf @getLocalPath(), (err) => 223 | return cleanDeferred.rejectWith @, [err] if err 224 | cleanDeferred.resolveWith @ 225 | 226 | cleanDeferred.promise() 227 | 228 | ### 229 | # Extracts the node-webkit archive. 230 | # 231 | # @param {String} input 232 | # @param {String} output 233 | # @return {Promise} 234 | # @api private 235 | ### 236 | extract: (input, output = @getLocalPath()) -> 237 | extractDeferred = dfd() 238 | # Check that the input file exists. 239 | unless fs.existsSync(input) 240 | return extractDeferred.rejectWith @, [ 241 | new Error("The specified input file '#{input}' does not exist") 242 | ] 243 | 244 | # Ensure that the extraction destination exists. 245 | mkdirp output, (err) => 246 | return extractDeferred.rejectWith @, [err] if err 247 | 248 | # If extension is .tar or .tar.gz use Utils.untar 249 | if path.extname(path.basename(input, '.gz')) is '.tar' 250 | extractMethod = Utils.untar 251 | # if extension is .zip use Utils.unzip 252 | else if path.extname(input) is '.zip' 253 | extractMethod = Utils.unzip 254 | # unknown extension, throw error 255 | else extractDeferred.rejectWith @, [new Error("Unknown extension #{path.extname(input)}")] 256 | 257 | # extract! 258 | if extractMethod 259 | extractMethod(input, output) 260 | .done () => 261 | extractDeferred.resolveWith @ 262 | .fail (err) => 263 | extractDeferred.rejectWith @, [err] 264 | else 265 | extractDeferred.rejectWith @, [new Error("No extract method")] 266 | 267 | # Always delete the archive after extraction or if the extraction fails 268 | extractDeferred.always () -> fs.unlinkSync input 269 | 270 | extractDeferred.promise() 271 | 272 | ### 273 | # Verifies that the nw and nwsnapshot binaries exist. 274 | # 275 | # @return {Boolean} 276 | # @api public 277 | ### 278 | verifyBinaries: () -> 279 | if @minimalBuild 280 | return fs.existsSync(@getNwBin()) 281 | else 282 | return fs.existsSync(@getSnapshotBin()) and fs.existsSync(@getNwBin()) 283 | 284 | ### 285 | # Ensures that the node-webkit distribution is available for use. 286 | # 287 | # @return {Promise} 288 | # @api public 289 | ### 290 | ensure: () -> 291 | ensureDeferred = dfd() 292 | @versionExists = fs.existsSync(@getLocalPath()) 293 | # Check if the version exists and verify that the binaries are present. 294 | if @versionExists and @verifyBinaries() 295 | if @minimalBuild 296 | ensureDeferred.resolveWith @, [null, @getNwBin()] 297 | else 298 | ensureDeferred.resolveWith @, [@getSnapshotBin(), @getNwBin()] 299 | else 300 | # Always delete the old directory, there might be something left over 301 | # from a bad extraction or something. Basically be sure to start from scratch. 302 | @cleanVersionDirectoryForPlatform() 303 | # Download the distribution 304 | .then(@download) 305 | # Extract the downloaded archive 306 | .then(@extract) 307 | # Check if the binaries exist and resolve/reject 308 | .done () -> 309 | if @verifyBinaries() 310 | if @minimalBuild 311 | ensureDeferred.resolveWith @, [null, @getNwBin()] 312 | else 313 | ensureDeferred.resolveWith @, [@getSnapshotBin(), @getNwBin()] 314 | else 315 | ensureDeferred.rejectWith @, [ 316 | new Error("The expected binaries couldn't be 317 | found in the downloaded archive.") 318 | ] 319 | # Something in the chain went wrong, reject the deferred. 320 | .fail (err) -> 321 | ensureDeferred.rejectWith @, [err] 322 | ensureDeferred.promise() -------------------------------------------------------------------------------- /src/pubsub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: This is just a pure copy paste mix between 3 | * axon.PubSocket and axon.SubSocket. 4 | */ 5 | 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | var Socket = require('axon').Socket 11 | , debug = require('debug')('axon:pubsub') 12 | , escape = require('escape-regexp') 13 | , slice = require('axon/lib/utils').slice; 14 | 15 | /** 16 | * Expose `PubSubSocket`. 17 | */ 18 | 19 | module.exports = PubSubSocket; 20 | 21 | /** 22 | * Initialize a new `PubSubSocket`. 23 | * 24 | * @api private 25 | */ 26 | 27 | function PubSubSocket() { 28 | Socket.call(this); 29 | this.subscriptions = []; 30 | } 31 | 32 | /** 33 | * Inherits from `Socket.prototype`. 34 | */ 35 | 36 | PubSubSocket.prototype.__proto__ = Socket.prototype; 37 | 38 | /** 39 | * Check if this socket has subscriptions. 40 | * 41 | * @return {Boolean} 42 | * @api public 43 | */ 44 | 45 | PubSubSocket.prototype.hasSubscriptions = function(){ 46 | return !! this.subscriptions.length; 47 | }; 48 | 49 | /** 50 | * Check if any subscriptions match `topic`. 51 | * 52 | * @param {String} topic 53 | * @return {Boolean} 54 | * @api public 55 | */ 56 | 57 | PubSubSocket.prototype.matches = function(topic){ 58 | for (var i = 0; i < this.subscriptions.length; ++i) { 59 | if (this.subscriptions[i].test(topic)) { 60 | return true; 61 | } 62 | } 63 | return false; 64 | }; 65 | 66 | /** 67 | * Message handler. 68 | * 69 | * @param {net.Socket} sock 70 | * @return {Function} closure(msg, mulitpart) 71 | * @api private 72 | */ 73 | 74 | PubSubSocket.prototype.onmessage = function(sock){ 75 | var self = this; 76 | var patterns = this.subscriptions; 77 | 78 | if (this.hasSubscriptions()) { 79 | return function(msg, multipart){ 80 | var topic = multipart 81 | ? msg[0].toString() 82 | : msg.toString(); 83 | 84 | if (!self.matches(topic)) return debug('not subscribed to "%s"', topic); 85 | self.emit.apply(self, ['message'].concat(msg)); 86 | } 87 | } 88 | 89 | return Socket.prototype.onmessage.call(this, sock); 90 | }; 91 | 92 | /** 93 | * Subscribe with the given `re`. 94 | * 95 | * @param {RegExp|String} re 96 | * @return {RegExp} 97 | * @api public 98 | */ 99 | 100 | PubSubSocket.prototype.subscribe = function(re){ 101 | debug('subscribe to "%s"', re); 102 | this.subscriptions.push(re = toRegExp(re)); 103 | return re; 104 | }; 105 | 106 | /** 107 | * Clear current subscriptions. 108 | * 109 | * @api public 110 | */ 111 | 112 | PubSubSocket.prototype.clearSubscriptions = function(){ 113 | this.subscriptions = []; 114 | }; 115 | 116 | /** 117 | * Send `msg` to all established peers. 118 | * 119 | * @param {Mixed} msg 120 | * @api public 121 | */ 122 | 123 | PubSubSocket.prototype.send = function(msg){ 124 | var socks = this.socks 125 | , len = socks.length 126 | , sock; 127 | 128 | if (arguments.length > 1) msg = slice(arguments); 129 | msg = this.pack(msg); 130 | 131 | for (var i = 0; i < len; i++) { 132 | sock = socks[i]; 133 | if (sock.writable) sock.write(msg); 134 | } 135 | 136 | return this; 137 | }; 138 | 139 | /** 140 | * Convert `str` to a `RegExp`. 141 | * 142 | * @param {String} str 143 | * @return {RegExp} 144 | * @api private 145 | */ 146 | 147 | function toRegExp(str) { 148 | if (str instanceof RegExp) return str; 149 | str = escape(str); 150 | str = str.replace(/\\\*/g, '(.+)'); 151 | return new RegExp('^' + str + '$'); 152 | } -------------------------------------------------------------------------------- /src/server.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Dependencies 3 | ### 4 | 5 | Config = require "./config" 6 | Snapshot = require "./snapshot" 7 | PubSubSocket = require "./pubsub" 8 | express = require 'express' 9 | 10 | ### 11 | # Server definition 12 | ### 13 | 14 | module.exports = 15 | http: null 16 | socket: null 17 | app: null 18 | start: () -> 19 | @socket = new PubSubSocket 20 | that = @ 21 | @socket.on 'message', (nwVersion, appSourceNw, snapshotSource, iterations) -> 22 | Snapshot.config { 23 | nwVersion: nwVersion.toString(), 24 | appSourceNw, 25 | snapshotSource, 26 | iterations: parseInt(iterations) 27 | } 28 | Snapshot.prepare() 29 | .then Snapshot.run 30 | .progress (status, tries) -> 31 | that.socket.send 'progress', status.toString(), tries.toString() 32 | .fail (err, tries) -> 33 | that.socket.send 'fail', err.toString(), tries?.toString() or '0' 34 | .done (snapshot, tries) -> 35 | that.socket.send 'done', snapshot, tries.toString() 36 | .always () -> 37 | Snapshot.resetState() 38 | 39 | @socket.bind(Config.sockPort, Config.hostIP) 40 | 41 | @app = express() 42 | 43 | @app.get '/callback/:id', (req, res) -> 44 | res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'); 45 | res.header('Expires', 'Fri, 31 Dec 1998 12:00:00 GMT'); 46 | Snapshot.notify req.params.id 47 | res.end() 48 | 49 | @http = @app.listen Config.httpPort 50 | 51 | return {@app, @http, @socket} 52 | 53 | stop: () -> 54 | @socket.close() 55 | @http.close() 56 | @http = null 57 | @app = null 58 | @socket = null -------------------------------------------------------------------------------- /src/snapshot.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Dependencies 3 | ### 4 | 5 | Config = require './config' 6 | Downloader = require './downloader' 7 | Utils = require './utils' 8 | path = require 'path' 9 | exec = require('child_process').exec 10 | dfd = require('jquery-deferred').Deferred 11 | mkdirp = require 'mkdirp' 12 | rimraf = require 'rimraf' 13 | fs = require 'fs' 14 | glob = require 'glob' 15 | 16 | ### 17 | # States 18 | ### 19 | 20 | STATE_READY = 0 21 | STATE_CONFIGURING = 1 22 | STATE_PREPARING = 2 23 | STATE_BUILDING = 3 24 | STATE_MAKINGTEST = 4 25 | STATE_TESTING = 5 26 | STATE_CLEANINGUP = 6 27 | 28 | ### 29 | # Snapshot definition 30 | ### 31 | 32 | module.exports = 33 | 34 | ### 35 | # Properties 36 | ### 37 | prepared: false 38 | state: STATE_READY 39 | outputFileName: 'snapshot.bin' 40 | outputFilePath: null 41 | execTimeout: null 42 | aborted: no 43 | tries: 0 44 | 45 | ### 46 | # Configures the snapshot object for building and testing. 47 | # NOTE: data.appSourceNw is a zip archive containing all files needed 48 | # to run the app (usually called app.nw). That is all assets and package.json. 49 | # 50 | # @param {Object} data 51 | # @returns {Promise} 52 | # @api public 53 | ### 54 | config: (data) -> 55 | @configurationDeferred = dfd() 56 | # Check if we're in the correct state, reject the deferred if not. 57 | unless @state is STATE_READY 58 | err = new Error("Build currently in process, please wait for it to complete, or send the 'abort' event.") 59 | @configurationDeferred.rejectWith @, [err] 60 | 61 | @state = STATE_CONFIGURING 62 | 63 | # Check if we recieved the data we need, reject the deferred if not. 64 | unless data.nwVersion and data.snapshotSource and data.appSourceNw 65 | err = new Error("Insufficient information, you must supply nwVersion, snapshotSource, appSourceNw.") 66 | @resetState() 67 | @configurationDeferred.rejectWith @, [err] 68 | 69 | # Default iterations if none specified. 70 | unless data.iterations? 71 | data.iterations = 1 72 | 73 | # Set local properties from the data object and resolve. 74 | {@nwVersion, @snapshotSource, @appSourceNw, @iterations} = data 75 | @prepared = false 76 | @configurationDeferred.resolveWith @ 77 | @configurationDeferred.promise() 78 | 79 | ### 80 | # Prepares the specified node-webkit version for compiling the snapshot. 81 | # 82 | # @returns {Promise} 83 | # @api private 84 | ### 85 | prepare: () -> 86 | @state = STATE_PREPARING 87 | @preparationDeferred = dfd() 88 | @download() 89 | .then(@makeTestDirectory) 90 | .then(@extractSource) 91 | .done () -> 92 | @preparationDeferred.resolveWith @ 93 | .fail (err) -> 94 | @preparationDeferred.rejectWith @, [err, @tries] 95 | @preparationDeferred.promise() 96 | 97 | ### 98 | # Extracts the application source code to the test directory, and 99 | # patches the package.json file to make use of the snapshot. 100 | # 101 | # @returns {Promise} 102 | # @api private 103 | ### 104 | extractSource: () -> 105 | @state = STATE_PREPARING 106 | @extractDeferred = dfd() 107 | mkdirp path.join(@testdir, 'src'), (err) => 108 | zipLocation = path.join(@testdir, 'src', 'app.zip') 109 | fs.writeFile zipLocation, @appSourceNw, 'binary', (err) => 110 | return unless @checkState @extractDeferred, STATE_PREPARING, err 111 | Utils.unzip zipLocation, path.join(@testdir, 'src') 112 | .done () => 113 | # Write snapshot information to package.json 114 | packagePath = path.join @testdir, 'src', 'package.json' 115 | packageJson = JSON.parse fs.readFileSync packagePath 116 | packageJson.snapshot = @outputFileName 117 | fs.writeFile packagePath, JSON.stringify(packageJson), (err) => 118 | return unless @checkState @extractDeferred, STATE_PREPARING, err 119 | @extractDeferred.resolveWith @ 120 | .fail (err) => 121 | @extractDeferred.rejectWith @, [err] 122 | @extractDeferred.promise() 123 | 124 | ### 125 | # Downloads the specified node-webkit version. 126 | # 127 | # @returns {Promise} 128 | # @api private 129 | ### 130 | download: () -> 131 | @downloadDeferred = dfd() 132 | 133 | # Download the specified node-webkit distribution 134 | downloader = new Downloader(@nwVersion) 135 | downloadPromise = downloader.ensure() 136 | 137 | downloadPromise.done (@snapshotterPath, @nwPath) => 138 | # We proxy the promise as we want to set the context, set local flags, 139 | # check if we need to abort, and not pass along the executables. 140 | if @checkState(@preparationDeferred, STATE_PREPARING) 141 | @prepared = true 142 | @downloadDeferred.resolveWith @ 143 | 144 | downloadPromise.fail (err) => 145 | # Download didn't go well, reset the state and reject the deferred. 146 | @resetState() 147 | @downloadDeferred.rejectWith @, [err] 148 | 149 | @downloadDeferred.promise() 150 | 151 | ### 152 | # Creates the test directory with application files. 153 | # 154 | # @returns {Promise} 155 | # @api private 156 | ### 157 | makeTestDirectory: () -> 158 | @makeDeferred = dfd() 159 | @state = STATE_MAKINGTEST 160 | @testdir = path.join __dirname, '..', "tmp", new Date().getTime() + "" 161 | 162 | # Make sure the testdir exists 163 | mkdirp @testdir, (err) => 164 | return unless @checkState(@makeDeferred, STATE_MAKINGTEST, err) 165 | @makeDeferred.resolveWith @,[ @testdir] 166 | 167 | @makeDeferred.promise() 168 | 169 | ### 170 | # Patches the snapshot source code with the build callback function. 171 | # The callback function is supposed to be invoked from the main .html file. 172 | # This is done to test the validity of the snapshot, to make sure it works. 173 | # 174 | # @returns {Promise} 175 | # @api private 176 | ### 177 | patchSource: () -> 178 | @patchDeferred = dfd() 179 | @state = STATE_BUILDING 180 | 181 | # Make an id to make sure we're called back from the right application 182 | # and not some random resurrected zombie node-webkit process from a previous test. 183 | # The id is used as a flag for launching the app, so that u need the build id to trigger 184 | # the build callback. 185 | @id = new Date().getTime() + '_' + Math.round(Math.random() * (1000 - 1) + 1) 186 | 187 | # Generate callback code 188 | callbackCode = """ 189 | // callback for build testing 190 | var __buildcallbackWrapper = function() { 191 | callbackArgIndex = require('nw.gui').App.argv.indexOf('--#{@id}'); 192 | if (callbackArgIndex > -1) { 193 | url = "#{Config.callbackURL}/#{@id}" 194 | script = document.createElement('script'); 195 | script.src = url; 196 | script.onload = function(){ 197 | process.exit(); 198 | }; 199 | document.querySelector('body').appendChild(script); 200 | } 201 | } 202 | """ 203 | # Write snapshot js with appended callback code 204 | fs.writeFile path.join(@testdir, 'snapshot.js'), @snapshotSource.toString() + callbackCode, (err) => 205 | return unless @checkState @patchDeferred, STATE_BUILDING, err 206 | @patchDeferred.resolveWith @ 207 | 208 | @patchDeferred.promise() 209 | 210 | parseVersion: () -> 211 | # Split on '-' to get rid of "rc" part if present. 212 | frags = @nwVersion.split('-')[0].split('.') 213 | major = parseInt frags[0] 214 | minor = parseInt frags[1] 215 | patch = parseInt frags[2] 216 | return {major, minor, patch} 217 | 218 | isNwjs: () -> 219 | {major, minor} = @parseVersion() 220 | (major is 0 and minor >= 12) or major > 0 221 | 222 | ### 223 | # Compiles the snapshot. 224 | # 225 | # @returns {Promise} 226 | # @api private 227 | ### 228 | compile: () -> 229 | @state = STATE_BUILDING 230 | @buildDeferred = dfd() 231 | 232 | @outputFilePath = path.join @testdir, @outputFileName 233 | @testFilePath = path.join(@testdir, 'src', path.basename @outputFileName) 234 | 235 | # Compile the snapshot 236 | @patchSource() 237 | .done () -> 238 | if @isNwjs() 239 | cmd = "#{@snapshotterPath} #{path.join @testdir, 'snapshot.js'} #{@outputFilePath}" 240 | else 241 | cmd = "#{@snapshotterPath} --extra_code #{path.join @testdir, 'snapshot.js'} #{@outputFilePath}" 242 | exec cmd, (err) => 243 | if @checkState(@buildDeferred, STATE_BUILDING, err) 244 | # Copy the snapshot to the test dir 245 | fs.readFile @outputFilePath, 'binary', (err, data) => 246 | return unless @checkState(@buildDeferred, STATE_BUILDING, err) 247 | fs.writeFile @testFilePath, data, 'binary', (err) => 248 | return unless @checkState(@buildDeferred, STATE_BUILDING, err) 249 | # Resolve the deferred, we're done. 250 | @buildDeferred.resolveWith @ 251 | .fail (err) -> 252 | @buildDeferred.rejectWith @, [err] 253 | 254 | 255 | @buildDeferred.promise() 256 | 257 | ### 258 | # Starts an iteration which will compile and test the snapshot. 259 | # 260 | # @returns {Promise} 261 | # @api private 262 | ### 263 | iterate: () -> 264 | @iterations-- 265 | @compile().then @test 266 | 267 | ### 268 | # Starts the snapshotter. 269 | # 270 | # @returns {Promise} 271 | # @api public 272 | ### 273 | run: () -> 274 | @tries = 0 275 | @runDeferred = dfd() 276 | 277 | unless @prepared 278 | @runDeferred.rejectWith @, [new Error("You need to run prepare() first!")] 279 | return @runDeferred.promise() 280 | 281 | doneFilter = () -> 282 | # Nothing to do here. Snapshot is good. 283 | 284 | failFilter = (err) -> 285 | # Notify about the failure 286 | @tries++ 287 | @runDeferred.notifyWith @, [err, @tries] 288 | # Delete old snapshot 289 | @cleanupSnapshot() 290 | # Try again and append the promise to the chain or fail if iterations are used up. 291 | if @iterations > 0 then @iterate().then.apply(@, filters) else err 292 | 293 | filters = [doneFilter, failFilter] 294 | 295 | # Start compilating and testing. 296 | @iterate().then.apply @, filters 297 | 298 | .done () -> 299 | # Snapshot test passed, clean up! 300 | # Read the snapshot into a buffer 301 | fileBuffer = fs.readFileSync @outputFilePath 302 | @cleanupTest().always () -> 303 | # Resolve the deferred, we were succesful! 304 | @runDeferred.resolveWith @, [fileBuffer, @tries] 305 | # Finally reset the state for the next job. 306 | @resetState() 307 | 308 | .fail (err) -> 309 | # Snapshot testing failed, clean up! 310 | @cleanupTest().always () -> 311 | @runDeferred.rejectWith @, [err, @tries] 312 | @resetState() 313 | 314 | @runDeferred.promise() 315 | 316 | ### 317 | # Checks if the snapshotter is in the correct state or if we need to abort. 318 | # An error object can be supplied for convenience when checking in callbacks. 319 | # 320 | # @param {Deferred} deferred. 321 | # @param {Integer} expectedState 322 | # @param {Error} err 323 | # @returns {Boolean} 324 | # @api private 325 | ### 326 | checkState: (deferred, expectedState, err = null) -> 327 | if err 328 | deferred.rejectWith @, [err] 329 | return false 330 | if @state isnt expectedState 331 | err = new Error("State mismatch. State was #{@state} expecting #{expectedState}.") 332 | deferred.rejectWith @, [err] 333 | return false 334 | if @aborted 335 | # If we need to abort we just reject the deferred so 336 | # everything will happen naturally. 337 | deferred.rejectWith @, [new Error('Aborted!')] 338 | return false 339 | return true 340 | 341 | ### 342 | # Cleans up (deletes) the test directory. 343 | # 344 | # @returns {Promise} 345 | # @api private 346 | ### 347 | cleanupTest: () -> 348 | @state = STATE_CLEANINGUP 349 | @cleanupDeferred = dfd() 350 | 351 | # Delete the test directory and all its content. 352 | rimraf @testdir, (err) => 353 | return @cleanupDeferred.rejectWith @, [err] if err 354 | 355 | glob "**/*v8.log", (err, files) => 356 | return @cleanupDeferred.rejectWith @, [err] if err 357 | for file in files 358 | try 359 | fs.unlinkSync file 360 | catch e 361 | return @cleanupDeferred.rejectWith @, [e] 362 | @cleanupDeferred.resolveWith @ 363 | 364 | @cleanupDeferred.promise() 365 | 366 | ### 367 | # Cleans up (deletes) the compiled snapshot. 368 | # 369 | # @returns {Boolean} result of unlink. 370 | # @api private 371 | ### 372 | cleanupSnapshot: () -> 373 | fs.unlinkSync @outputFilePath if fs.existsSync @outputFilePath 374 | 375 | ### 376 | # Notifies the snapshotter when the app has launced succesfully. 377 | # This method should be called from the server when the callback URL is requested. 378 | # Calling this method will immediately kill the app, as it's no longer needed. 379 | # 380 | # @returns {Boolean} always true. 381 | # @api public 382 | ### 383 | notify: (id) -> 384 | if @id is id 385 | @didNotify = yes 386 | @killProcess() 387 | true 388 | 389 | killProcess: () -> 390 | if Config.platform is 'win' 391 | # It seems impossible to kill nw.exe processes on windows. 392 | # Hit it with all we've got! 393 | # youtube.com/watch?v=74BzSTQCI_c 394 | exec "taskkill /pid #{@process.pid} /f" 395 | try 396 | @process.kill('SIGKILL') 397 | @process.kill('SIGTERM') 398 | @process.kill('SIGHUP') 399 | @process.kill() 400 | @process.exit() 401 | catch 402 | else 403 | @process.kill() 404 | 405 | ### 406 | # Launces the app with the compiled snapshot. 407 | # NOTE: patchSource/compile has to be run first to generate an id and callback code. 408 | # 409 | # @returns {Promise} 410 | # @api private 411 | ### 412 | launch: () -> 413 | @state = STATE_TESTING 414 | @launchDeferred = dfd() 415 | @didNotify = false 416 | 417 | unless @id 418 | @launchDeferred.rejectWith @, [new Error("Can't launch the test without a test id. See @compile() and @patchSource().")] 419 | 420 | # Execute the application 421 | exePath = path.join @testdir, 'src' 422 | @process = exec """#{@nwPath} "--#{@id}" #{exePath}""" 423 | 424 | # Set a timeout, we don't want to wait for the application forever. 425 | @execTimeout = setTimeout () => 426 | @killProcess() 427 | @launchDeferred.rejectWith @, [new Error("Timeout in testing after #{Config.timeout}ms.")] 428 | , Config.timeout 429 | 430 | # When the process exits, check if we we're called back 431 | @process.on 'exit', () => 432 | if @didNotify 433 | @launchDeferred.resolveWith @ 434 | @didNotify = false 435 | else 436 | clearTimeout @execTimeout 437 | @launchDeferred.rejectWith @, [new Error(""" 438 | Process exited without calling back. Probably just another bad snapshot. 439 | Could also be that you are not calling __buildcallbackWrapper() from your main html file. 440 | """)] 441 | 442 | @launchDeferred.promise() 443 | 444 | ### 445 | # Tests the compiled snapshot. 446 | # 447 | # @returns {Promise} 448 | # @api private 449 | ### 450 | test: () -> 451 | @testDeferred = dfd() 452 | @launch().fail (err) -> 453 | # Testing failed, clean up the snapshot so somebody doesn't 454 | # accidentally use it some where. 455 | @cleanupSnapshot() 456 | @testDeferred.rejectWith @, [err] 457 | .done () -> 458 | # Testing succeeded. 459 | @testDeferred.resolveWith @ 460 | @testDeferred.promise() 461 | 462 | ### 463 | # Resets the snapshotter object's state. 464 | # 465 | # @returns {Boolean} always true 466 | # @api private 467 | ### 468 | resetState: () -> 469 | @aborted = no 470 | @prepared = false 471 | @state = STATE_READY 472 | true 473 | 474 | ### 475 | # Calls resetState and resolves the abortDeferred. 476 | # 477 | # @returns {Deferred} 478 | # @api private 479 | ### 480 | resetStateAndResolve: () -> 481 | @resetState() 482 | @abortDeferred.resolveWith @ 483 | 484 | ### 485 | # Aborts current process, and properly cleans up. 486 | # This method is called from the server when an 'abort' event is recieved. 487 | # 488 | # @returns {Promise} 489 | # @api public 490 | ### 491 | abort: () -> 492 | console.log 'abort!' 493 | @abortDeferred = dfd() 494 | @aborted = yes 495 | # The deferreds will fail once an async method is completed because of @aborted. 496 | switch @state 497 | when STATE_READY then @resetStateAndResolve() 498 | when STATE_CONFIGURING then @configurationDeferred.always @resetStateAndResolve 499 | when STATE_PREPARING then @preparationDeferred.always @resetStateAndResolve 500 | when STATE_BUILDING then @buildDeferred.always @resetStateAndResolve 501 | when STATE_MAKINGTEST then @makeDeferred.always @resetStateAndResolve 502 | when STATE_TESTING 503 | @launchDeferred.always @resetStateAndResolve 504 | # Kill the app to speed the process along. 505 | @killProcess() 506 | 507 | @abortDeferred.promise() 508 | -------------------------------------------------------------------------------- /src/utils.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Dependencies 3 | ### 4 | 5 | Config = require './config' 6 | fs = require 'fs' 7 | dfd = require('jquery-deferred').Deferred 8 | DecompressZip = require 'decompress-zip' 9 | tar = require 'tar' 10 | exec = require('child_process').exec 11 | zlib = require "zlib" 12 | 13 | ### 14 | # Utils definition 15 | ### 16 | 17 | module.exports = 18 | 19 | ### 20 | # Unzip input into output. 21 | # Uses native unzip on osx and js implementation on linux and win 22 | # 23 | # @param {String} input 24 | # @param {String} output 25 | # @return {Promise} 26 | # @api private 27 | ### 28 | unzip: (input, output) -> 29 | throw new Error "No input file specifed" unless input 30 | throw new Error "No ouput folder specified" unless output 31 | unzipDeferred = dfd() 32 | # Need to know which platform we're on which is 33 | # why we don't use @platform. 34 | if Config.platform is 'osx' 35 | # Use native unzip 36 | exec "unzip -o '#{input}' -d '#{output}'", {cwd: output}, (err) => 37 | return unzipDeferred.rejectWith @, [err] if err 38 | unzipDeferred.resolveWith @ 39 | else 40 | # Unzip is not guaranteed on linux so use js unzip implementation. 41 | # Unfortunately most node.js zip implementations are 42 | # extremely flaky. 43 | unzip = new DecompressZip input 44 | unzip.on 'error', (err) => 45 | unzipDeferred.rejectWith @, [err] 46 | unzip.on 'extract', () => 47 | unzipDeferred.resolveWith @ 48 | unzip.extract {path: output} 49 | 50 | unzipDeferred.promise() 51 | 52 | ### 53 | # Untar input into output. 54 | # Uses native tar on osx and linux and js implementation on win 55 | # 56 | # @param {String} input 57 | # @param {String} output 58 | # @return {Promise} 59 | # @api private 60 | ### 61 | untar: (input, output) -> 62 | untarDeferred = dfd() 63 | throw new Error "No input file specifed" unless input 64 | throw new Error "No ouput folder specified" unless output 65 | # Need to know which platform we're on which is 66 | # why we don't use @platform. 67 | if Config.platform in ['osx', 'linux'] 68 | # Use native tar 69 | exec "tar -xf '#{input}'", {cwd: output}, (err) => 70 | return untarDeferred.rejectWith @, [err] if err 71 | untarDeferred.resolveWith @ 72 | else 73 | # Baaaah windows.. Use js tar implementation 74 | # This is *incredibly* slow (150-300s).. But seems to work for now. 75 | src = fs.createReadStream input 76 | src.pipe(zlib.createGunzip()).pipe tar.Extract path: output 77 | .on 'end', () => untarDeferred.resolveWith @ 78 | .on 'error', (err) => untarDeferred.rejectWith @, [err] 79 | 80 | untarDeferred.promise() -------------------------------------------------------------------------------- /test/communication.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Dependencies 3 | ### 4 | 5 | should = require 'should' 6 | {Snapshot, Config, Server, Client} = require '../index' 7 | rimraf = require 'rimraf' 8 | fs = require 'fs' 9 | path = require 'path' 10 | dfd = require('jquery-deferred').Deferred 11 | 12 | ### 13 | # Fixtures 14 | ### 15 | 16 | fixtures = 17 | app: null 18 | snapshotSource: null 19 | iterations: 1 20 | 21 | {nwVersion} = require './config' 22 | 23 | ### 24 | # Tests 25 | ### 26 | 27 | describe "Client / Server", () -> 28 | 29 | client = null 30 | 31 | before (done) -> 32 | fixtures.app = fs.readFileSync (path.join __dirname, 'fixtures', 'app.zip') 33 | fixtures.snapshotSource = fs.readFileSync (path.join __dirname, 'fixtures', 'snapshot.js') 34 | Server.start() 35 | client = new Client nwVersion, fixtures.app, fixtures.snapshotSource 36 | client.connect "tcp://127.0.0.1:#{Config.sockPort}", done 37 | 38 | after () -> 39 | client.disconnect() 40 | Server.stop() 41 | 42 | describe "pubsub socket", () -> 43 | 44 | it "server should build when message is recieved and report back to client", (done) -> 45 | 46 | # Mock the snapshot config function so we will know 47 | # if the server starts to build. 48 | oldConfig = Snapshot.config 49 | oldPrepare = Snapshot.prepare 50 | Snapshot.config = (opts) -> 51 | opts.nwVersion.should.be.equal nwVersion 52 | opts.appSourceNw.length.should.be.equal fixtures.app.length 53 | opts.snapshotSource.length.should.be.equal fixtures.snapshotSource.length 54 | Snapshot.prepare = () -> 55 | return dfd().reject(new Error('mock!'), 0).promise() 56 | 57 | client.on 'fail', () -> 58 | Snapshot.config = oldConfig 59 | Snapshot.prepare = oldPrepare 60 | client.removeAllListeners() 61 | done() 62 | 63 | client.build fixtures.iterations 64 | 65 | 66 | describe "http callback route", () -> 67 | 68 | it "should notify the snapshotter when requested", (done) -> 69 | 70 | sentID = "test" 71 | oldNotify = Snapshot.notify 72 | Snapshot.notify = (receivedID) -> 73 | receivedID.should.be.equal sentID 74 | Snapshot.notify = oldNotify 75 | done() 76 | 77 | require('request').get "http://127.0.0.1:#{Config.httpPort}/callback/test", (err, response, body) -> 78 | throw err if err 79 | response.statusCode.should.be.equal 200 80 | 81 | describe "all", () -> 82 | this.timeout(120000) 83 | it "should do everything (stupid catch all test)", (done) -> 84 | client.build 5 85 | # client.on 'progress', (status, tries) -> console.log 'progress:', status, tries 86 | client.on 'fail', (err, tries) -> 87 | # console.log 'fail:', err, tries 88 | done() 89 | client.on 'done', (snapshot, tries) -> 90 | # console.log 'done:', snapshot, tries 91 | done() 92 | after () -> 93 | client.removeAllListeners() 94 | -------------------------------------------------------------------------------- /test/config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nwVersion: '0.25.3' 3 | } -------------------------------------------------------------------------------- /test/downloader.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Dependencies 3 | ### 4 | 5 | should = require 'should' 6 | {Downloader} = require '../index' 7 | rimraf = require 'rimraf' 8 | fs = require 'fs' 9 | path = require 'path' 10 | 11 | ### 12 | # Fixtures 13 | ### 14 | 15 | binFolder = 'test_bin' 16 | {nwVersion} = require './config' 17 | 18 | ### 19 | # Tests 20 | ### 21 | 22 | describe "NodeWebkitDownloader", () -> 23 | 24 | after (done) -> 25 | rimraf path.join(__dirname, '..', binFolder), (err) -> 26 | # See https://github.com/miklschmidt/node-nw-snapshot/issues/1 27 | if err and process.platform.match(/^win/) 28 | return done() # Yea... well.. it doesn't work here.. 29 | else if err 30 | throw err 31 | done() 32 | 33 | before (done) -> 34 | if fs.existsSync(path.join __dirname, '..', binFolder) 35 | rimraf path.join(__dirname, '..', binFolder), (err) -> 36 | throw err if err 37 | done() 38 | else 39 | done() 40 | 41 | describe "#constructor", () -> 42 | it 'should throw errors when version is undefined', () -> 43 | try 44 | downloader = new Downloader 45 | catch e 46 | err = e 47 | should.exist err 48 | 49 | it 'should properly set platform and arch without parameters', () -> 50 | downloader = new Downloader nwVersion 51 | if process.platform.match(/^darwin/) 52 | platform = 'osx' 53 | else if process.platform.match(/^win/) 54 | platform = 'win' 55 | else 56 | platform = 'linux' 57 | 58 | arch = process.arch 59 | 60 | downloader.platform.should.equal platform 61 | downloader.arch.should.equal arch 62 | 63 | it 'should throw errors when supplied invalid platform or arch', () -> 64 | try 65 | downloader = new Downloader nwVersion, 'bogusPlatform', 'bogusArch' 66 | catch e 67 | err = e 68 | should.exist err 69 | 70 | # Conditional test.. probably not the best way to handle this.. 71 | if process.platform.match(/^win/) 72 | it 'should throw errors when supplying linux as platform on windows', () -> 73 | try 74 | downloader = new Downloader nwVersion, 'linux', 'x64' 75 | catch e 76 | x64error = e 77 | try 78 | downloader = new Downloader nwVersion, 'linux', 'ia32' 79 | catch e 80 | ia32error = e 81 | should.exist x64error 82 | should.exist ia32error 83 | 84 | describe "#getDownloadURL", () -> 85 | it 'should return a valid download url', (done) -> 86 | downloader = new Downloader nwVersion 87 | url = downloader.getDownloadURL() 88 | require('request').head url, (err, response, body) -> 89 | should.not.exist err 90 | should.exist body 91 | response.statusCode.should.equal 200 92 | done() 93 | return null 94 | 95 | it 'should return a valid minimal download url', (done) -> 96 | downloader = new Downloader nwVersion, undefined, undefined, true 97 | url = downloader.getDownloadURL() 98 | require('request').head url, (err, response, body) -> 99 | should.not.exist err 100 | should.exist body 101 | response.statusCode.should.equal 200 102 | done() 103 | return null 104 | 105 | describe "#download", () -> 106 | it 'should resolve the promise when downloaded', (done) -> 107 | this.timeout(60000) 108 | downloader = new Downloader nwVersion 109 | downloader.binFolder = binFolder 110 | 111 | doneCalled = false 112 | failCalled = false 113 | downloader.download() 114 | .done () -> doneCalled = true 115 | .fail () -> failCalled = true 116 | .always () -> 117 | doneCalled.should.be.true 118 | failCalled.should.be.false 119 | done() 120 | return null 121 | 122 | it 'should be able to download minimal distribution', () -> 123 | this.timeout(60000) 124 | downloader = new Downloader nwVersion, undefined, undefined, true 125 | downloader.binFolder = binFolder 126 | downloader.download() 127 | 128 | it 'should reject the promise when download failed', (done) -> 129 | downloader = new Downloader '9999.99999.9999' # useless version number to force a fail. 130 | downloader.binFolder = binFolder 131 | doneCalled = false 132 | failCalled = false 133 | downloader.download() 134 | .done () -> doneCalled = true 135 | .fail () -> failCalled = true 136 | .always () -> 137 | doneCalled.should.be.false 138 | failCalled.should.be.true 139 | # Remove the bogus directory created with the crazy version number 140 | rimraf downloader.getLocalPath(), (err) -> 141 | done() 142 | return null 143 | 144 | 145 | it 'should resolve the promise even if the download already exists', (done) -> 146 | # NOTE: This is dependent on the first #download test passing 147 | downloader = new Downloader nwVersion 148 | downloader.binFolder = binFolder 149 | doneCalled = false 150 | failCalled = false 151 | downloader.download() 152 | .done () -> doneCalled = true 153 | .fail () -> failCalled = true 154 | .always () -> 155 | doneCalled.should.be.true 156 | failCalled.should.be.false 157 | done() 158 | return null 159 | 160 | describe "#extract", () -> 161 | this.timeout(60000) 162 | 163 | # NOTE: This is dependent on the #download tests passing 164 | # should probably fix this and supply archives for proper testing. 165 | testExtraction = (platform, arch) -> 166 | downloader = new Downloader nwVersion, platform, arch 167 | downloader.binFolder = binFolder 168 | 169 | promise = downloader.download().then((destinationFile) => return downloader.extract(destinationFile)) 170 | .done () -> 171 | downloader.verifyBinaries().should.be.true 172 | .fail (err) -> 173 | # fails the test 174 | throw err 175 | return promise 176 | 177 | it 'should be able to extract osx-x64 archive', () -> testExtraction('osx', 'x64') 178 | it 'should be able to extract win-ia32 archive', () -> testExtraction('win', 'ia32') 179 | 180 | # Conditional test.. probably not the best way to handle this.. 181 | unless process.platform.match(/^win/) 182 | it 'should be able to extract linux-ia32 archive', () -> testExtraction('linux', 'ia32') 183 | it 'should be able to extract linux-x64 archive', () -> testExtraction('linux', 'x64') 184 | 185 | describe "#ensure", () -> 186 | this.timeout(60000) 187 | testEnsure = (platform, arch) -> 188 | downloader = new Downloader nwVersion, platform, arch 189 | downloader.binFolder = binFolder 190 | 191 | return downloader.ensure() 192 | 193 | 194 | it 'should be able to ensure that a specified version is available for osx-ia32', () -> testEnsure('osx', 'x64') 195 | it 'should be able to ensure that a specified version is available for win-ia32', () -> testEnsure('win', 'ia32') 196 | # Conditional test.. probably not the best way to handle this.. 197 | unless process.platform.match(/^win/) 198 | it 'should be able to ensure that a specified version is available for linux-ia32', () -> testEnsure('linux', 'ia32') 199 | it 'should be able to ensure that a specified version is available for linux-x64', () -> testEnsure('linux', 'x64') 200 | 201 | it 'should be able to ensure minimal distribution', () -> 202 | downloader = new Downloader nwVersion, undefined, undefined, true 203 | downloader.binFolder = binFolder 204 | downloader.ensure() 205 | 206 | 207 | describe "#cleanVersionDirectoryForPlatform", () -> 208 | it 'should delete the directory', () -> 209 | downloader = new Downloader nwVersion 210 | downloader.binFolder = binFolder 211 | doneCalled = false 212 | failCalled = false 213 | 214 | fs.existsSync(downloader.getLocalPath()).should.be.true 215 | 216 | promise = downloader.cleanVersionDirectoryForPlatform() 217 | .done () -> 218 | doneCalled = true 219 | .fail (err) -> 220 | failCalled = true 221 | throw err 222 | .always () -> 223 | doneCalled.should.be.true 224 | failCalled.should.be.false 225 | fs.existsSync(downloader.getLocalPath()).should.be.false 226 | 227 | -------------------------------------------------------------------------------- /test/fixtures/app.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miklschmidt/node-nw-snapshot/cabd2060dfc44d2aa43988d978f3171683ceb33d/test/fixtures/app.zip -------------------------------------------------------------------------------- /test/fixtures/snapshot.js: -------------------------------------------------------------------------------- 1 | var run = function () { 2 | function updateCheckbox() { 3 | var top_checkbox = document.getElementById("top-box"); 4 | var bottom_checkbox = document.getElementById("bottom-box"); 5 | var left_checkbox = document.getElementById("left-box"); 6 | var right_checkbox = document.getElementById("right-box"); 7 | if (top_checkbox.checked || bottom_checkbox.checked) { 8 | left_checkbox.disabled = true; 9 | right_checkbox.disabled = true; 10 | } else if (left_checkbox.checked || right_checkbox.checked) { 11 | top_checkbox.disabled = true; 12 | bottom_checkbox.disabled = true; 13 | } else { 14 | left_checkbox.disabled = false; 15 | right_checkbox.disabled = false; 16 | top_checkbox.disabled = false; 17 | bottom_checkbox.disabled = false; 18 | } 19 | } 20 | 21 | function initCheckbox(checkboxId, titlebar_name, titlebar_icon_url, titlebar_text) { 22 | var elem = document.getElementById(checkboxId); 23 | if (!elem) 24 | return; 25 | elem.onclick = function() { 26 | if (document.getElementById(checkboxId).checked) 27 | addTitlebar(titlebar_name, titlebar_icon_url, titlebar_text); 28 | else 29 | removeTitlebar(titlebar_name); 30 | focusTitlebars(true); 31 | 32 | updateContentStyle(); 33 | updateCheckbox(); 34 | } 35 | } 36 | 37 | window.onfocus = function() { 38 | console.log("focus"); 39 | focusTitlebars(true); 40 | } 41 | 42 | window.onblur = function() { 43 | console.log("blur"); 44 | focusTitlebars(false); 45 | } 46 | 47 | window.onresize = function() { 48 | updateContentStyle(); 49 | } 50 | 51 | window.onload = function() { 52 | initCheckbox("top-box", "top-titlebar", "top-titlebar.png", "Top Titlebar"); 53 | initCheckbox("bottom-box", "bottom-titlebar", "bottom-titlebar.png", "Bottom Titlebar"); 54 | initCheckbox("left-box", "left-titlebar", "left-titlebar.png", "Left Titlebar"); 55 | initCheckbox("right-box", "right-titlebar", "right-titlebar.png", "Right Titlebar"); 56 | 57 | document.getElementById("close-window-button").onclick = function() { 58 | window.close(); 59 | } 60 | 61 | updateContentStyle(); 62 | require("nw.gui").Window.get().show(); 63 | } 64 | } -------------------------------------------------------------------------------- /test/nwsnapshot.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Dependencies 3 | ### 4 | 5 | should = require 'should' 6 | {Config, Server, Client} = require '../index' 7 | fs = require 'fs' 8 | path = require 'path' 9 | 10 | {nwVersion} = require './config' 11 | 12 | ### 13 | # Fixtures 14 | ### 15 | 16 | fixtures = 17 | app: null 18 | snapshotSource: null 19 | iterations: 50 20 | 21 | ### 22 | # Tests 23 | ### 24 | 25 | describe "nwsnapshot binary", () -> 26 | 27 | client = null 28 | 29 | before (done) -> 30 | fixtures.app = fs.readFileSync (path.join __dirname, 'fixtures', 'app.zip') 31 | fixtures.snapshotSource = fs.readFileSync (path.join __dirname, 'fixtures', 'snapshot.js') 32 | Server.start() 33 | client = new Client nwVersion, fixtures.app, fixtures.snapshotSource 34 | client.connect "tcp://127.0.0.1:#{Config.sockPort}", done 35 | 36 | after () -> 37 | client.disconnect() 38 | Server.stop() 39 | 40 | this.timeout(1000 * 60 * 10) # 10 minutes 41 | 42 | it "Should compile a valid snapshot each time (test nwsnapshotter)", (done) -> 43 | n = fixtures.iterations 44 | fails = 0 45 | wins = 0 46 | final = () -> 47 | wins.should.be.equal 50 48 | fails.should.be.equal 0 49 | console.log fails, wins 50 | done() 51 | 52 | client.build 1 53 | client.on 'fail', (err, tries) -> 54 | fails++ 55 | if --n then client.build 1 else final() 56 | client.on 'done', (snapshot, tries) -> 57 | wins++ 58 | if --n then client.build 1 else final() -------------------------------------------------------------------------------- /test/setup.coffee: -------------------------------------------------------------------------------- 1 | require('./downloader.coffee') 2 | # require('./snapshot.coffee') 3 | # require('./communication.coffee') -------------------------------------------------------------------------------- /test/snapshot.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | # Dependencies 3 | ### 4 | 5 | should = require 'should' 6 | {Snapshot, Config} = require '../index' 7 | rimraf = require 'rimraf' 8 | fs = require 'fs' 9 | path = require 'path' 10 | 11 | {nwVersion} = require './config' 12 | 13 | ### 14 | # Fixtures 15 | ### 16 | 17 | fixtures = 18 | app: null 19 | snapshotSource: null 20 | iterations: 5 21 | 22 | ### 23 | # Tests 24 | ### 25 | 26 | # TODO: Add test for snapshot callback url! 27 | 28 | describe "Snapshot", () -> 29 | 30 | before () -> 31 | fixtures.app = fs.readFileSync (path.join __dirname, 'fixtures', 'app.zip'), 'binary' 32 | fixtures.snapshotSource = fs.readFileSync (path.join __dirname, 'fixtures', 'snapshot.js') 33 | 34 | describe "#constructor", () -> 35 | 36 | it 'should reject the deferred when data is missing', (done) -> 37 | failCalled = false 38 | doneCalled = false 39 | Snapshot.config {} 40 | .fail () -> 41 | failCalled = true 42 | .done () -> 43 | doneCalled = true 44 | .always () -> 45 | failCalled.should.be.true 46 | doneCalled.should.be.false 47 | done() 48 | 49 | it 'should resolve the deferred when configured correctly', (done) -> 50 | failCalled = false 51 | doneCalled = false 52 | Snapshot.config 53 | nwVersion: nwVersion 54 | appSourceNw: fixtures.app 55 | snapshotSource: fixtures.snapshotSource 56 | iterations: fixtures.iterations 57 | 58 | .fail (err) -> 59 | throw err 60 | failCalled = true 61 | .done () -> 62 | doneCalled = true 63 | .always () -> 64 | failCalled.should.be.false 65 | doneCalled.should.be.true 66 | Snapshot.prepared.should.be.false 67 | done() 68 | 69 | describe "#prepare", () -> 70 | 71 | it 'should resolve the deferred', () -> 72 | this.timeout(60000) 73 | doneCalled = false 74 | failCalled = false 75 | Snapshot.prepare() 76 | .fail (err) -> 77 | throw err 78 | failCalled = true 79 | .done () -> 80 | doneCalled = true 81 | .always () -> 82 | failCalled.should.be.false 83 | doneCalled.should.be.true 84 | 85 | it 'should make test directory, and extract the source', () -> 86 | Snapshot.testdir.should.exist 87 | fs.existsSync(Snapshot.testdir).should.be.true 88 | 89 | it 'should extract the source', () -> 90 | fs.existsSync(path.join Snapshot.testdir, 'src').should.be.true 91 | fs.existsSync(path.join Snapshot.testdir, 'src', 'package.json').should.be.true 92 | 93 | it 'should write the snapshot path to package.json', () -> 94 | Snapshot.outputFileName.should.exist 95 | packagePath = path.join Snapshot.testdir, 'src', 'package.json' 96 | packageJson = JSON.parse fs.readFileSync packagePath 97 | packageJson.snapshot.should.equal Snapshot.outputFileName 98 | 99 | it 'should have the node-webkit executables handy', () -> 100 | fs.existsSync(Snapshot.snapshotterPath).should.be.true 101 | fs.existsSync(Snapshot.nwPath).should.be.true 102 | 103 | describe "#compile", () -> 104 | it 'should resolve the deferred', () -> 105 | Snapshot.compile() 106 | 107 | it 'should generate a test id', () -> 108 | Snapshot.id.should.exist 109 | 110 | it 'should write snapshot.js to test dir', () -> 111 | fs.existsSync(path.join(Snapshot.testdir, 'snapshot.js')).should.be.true 112 | 113 | it 'should generate the snapshot', () -> 114 | Snapshot.outputFilePath.should.exist 115 | fs.existsSync(Snapshot.outputFilePath).should.be.true 116 | 117 | it 'should copy the snapshot to the test directory', () -> 118 | Snapshot.testFilePath.should.exist 119 | fs.existsSync(Snapshot.testFilePath).should.be.true 120 | 121 | describe "#launch", () -> 122 | 123 | it 'should resolve when called back', (done) -> 124 | 125 | doneCalled = false 126 | failCalled = false 127 | 128 | Snapshot.launch() 129 | .done () -> doneCalled = true 130 | .fail () -> failCalled = true 131 | .always () -> 132 | doneCalled.should.be.true 133 | failCalled.should.be.false 134 | done() 135 | 136 | Snapshot.process.should.exist 137 | # Should kill the process immediately and resolve 138 | Snapshot.notify Snapshot.id 139 | 140 | it 'should reject if not called back', (done) -> 141 | 142 | doneCalled = false 143 | failCalled = false 144 | 145 | Snapshot.launch() 146 | .done () -> doneCalled = true 147 | .fail () -> failCalled = true 148 | .always () -> 149 | doneCalled.should.be.false 150 | failCalled.should.be.true 151 | done() 152 | 153 | Snapshot.process.should.exist 154 | Snapshot.process.kill() 155 | 156 | it 'should timeout if nothing happens', (done) -> 157 | # Set the timeout to be ridiculously low, so we fail pretty much instantly. 158 | oldTimeout = Config.timeout 159 | Config.timeout = 1 160 | 161 | doneCalled = false 162 | failCalled = false 163 | 164 | Snapshot.launch() 165 | .done () -> doneCalled = true 166 | .fail () -> failCalled = true 167 | .always () -> 168 | doneCalled.should.be.false 169 | failCalled.should.be.true 170 | done() 171 | 172 | # Put the original timeout back 173 | Config.timeout = oldTimeout 174 | Snapshot.process.should.exist 175 | 176 | describe "#test", () -> 177 | 178 | it 'should clean up snapshot when failed', (done) -> 179 | # Set the timeout to be ridiculously low, so we fail pretty much instantly. 180 | oldTimeout = Config.timeout 181 | Config.timeout = 1 182 | 183 | doneCalled = false 184 | failCalled = false 185 | 186 | Snapshot.test() 187 | .done () -> doneCalled = true 188 | .fail () -> failCalled = true 189 | .always () -> 190 | doneCalled.should.be.false 191 | failCalled.should.be.true 192 | fs.existsSync(Snapshot.outputFilePath).should.be.false 193 | 194 | # Put the original timeout back 195 | Config.timeout = oldTimeout 196 | Snapshot.process.should.exist 197 | done() 198 | return null 199 | 200 | 201 | describe "#run", () -> 202 | 203 | it "should iterate and notify", (done) -> 204 | this.timeout(10000) 205 | # Set the timeout to be ridiculously low, so we fail pretty much instantly. 206 | oldTimeout = Config.timeout 207 | Config.timeout = 1 208 | 209 | doneCalled = false 210 | failCalled = false 211 | 212 | notifications = 0 213 | Snapshot.run() 214 | .progress (err, tries) -> 215 | notifications++ 216 | .done () -> 217 | doneCalled = true 218 | .fail () -> 219 | failCalled = true 220 | .always () -> 221 | notifications.should.equal fixtures.iterations 222 | doneCalled.should.be.false 223 | failCalled.should.be.true 224 | 225 | # Put the original timeout back 226 | Config.timeout = oldTimeout 227 | done() 228 | return null 229 | 230 | it "should reject the deferred when not prepared/state mismatch", (done) -> 231 | doneCalled = false 232 | failCalled = false 233 | 234 | Snapshot.prepared.should.be.false 235 | 236 | Snapshot.run() 237 | .done () -> 238 | doneCalled = true 239 | .fail (err) -> 240 | failCalled = true 241 | .always () -> 242 | doneCalled.should.be.false 243 | failCalled.should.be.true 244 | done(); 245 | return null 246 | 247 | it "should resolve with snapshot when succesful", () -> 248 | doneCalled = false 249 | failCalled = false 250 | 251 | oldLaunch = Snapshot.launch 252 | 253 | # Mock the launch function and notify immediately 254 | Snapshot.launch = () -> 255 | result = oldLaunch.apply Snapshot, arguments 256 | Snapshot.notify Snapshot.id 257 | return result 258 | # Prepare the snapshotter 259 | Snapshot.prepare() 260 | # RUN! 261 | .then Snapshot.run 262 | .done () -> 263 | doneCalled = true 264 | .fail (err) -> 265 | throw err 266 | failCalled = true 267 | .always () -> 268 | doneCalled.should.be.true 269 | failCalled.should.be.false 270 | 271 | # Put the original launch function back 272 | Snapshot.launch = oldLaunch 273 | --------------------------------------------------------------------------------