├── .dockerignore ├── .gitignore ├── calypso.json ├── lib ├── github-hooks.js ├── project │ ├── index.js │ ├── project.js │ ├── branch.js │ └── mirror-repository.js ├── ws-app.js ├── server.js ├── index.js ├── views │ └── boot.pug ├── proxy-middlewares.js ├── worker.js └── proxy-manager.js ├── Makefile ├── package.json ├── Dockerfile ├── README.md └── LICENSE.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | tmp 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | .unison 3 | .env 4 | .DS_Store 5 | .idea 6 | *.iml 7 | *.iws 8 | tags 9 | node_modules 10 | /npm-debug.log* 11 | /package-lock.json 12 | 13 | data/ 14 | tmp/ 15 | -------------------------------------------------------------------------------- /calypso.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": { 3 | "type": "git", 4 | "url": "git+https://github.com/Automattic/wp-calypso.git" 5 | }, 6 | "env": { 7 | "NODE_ENV": "development", 8 | "CALYPSO_ENV": "development", 9 | "NODE_PATH": "server:client:.", 10 | "CALYPSO_LIVE_DEFAULT_CONFIG": "{ \"readyEvent\": \"compiler done\", \"format\": \"html\", \"maxBranches\": 100 }" 11 | }, 12 | "main": "build/bundle.js", 13 | "scripts": { 14 | "postinstall": "npm run build" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/github-hooks.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require( 'express' ); 3 | var bodyParser = require( 'body-parser' ); 4 | 5 | module.exports = function( config ) { 6 | var app = express(); 7 | 8 | app.use( bodyParser.json() ); 9 | app.use( bodyParser.urlencoded( { extended: true } ) ); 10 | 11 | app.post( '/push', function( req, res ) { 12 | var payload = req.body; 13 | if ( payload && payload.repository && payload.repository.name === '' ) { 14 | 15 | } 16 | } ); 17 | 18 | app.listen( 3001, function() { 19 | console.log( 'Github webhooks set up on port 3001' ); 20 | } ); 21 | 22 | return app; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/project/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var debug = require('debug')('clb-project'); 3 | 4 | var Project = require('./project'); 5 | 6 | var projects = {}; 7 | 8 | module.exports = function(config) { 9 | if ( ! projects[ config.name ] ) { 10 | debug( 'Creating project ' + config.name ); 11 | projects[ config.name ] = new Project({ 12 | name: config.name, 13 | repository: config.repository, 14 | destination: path.resolve(process.env.TMP_DIR || '/tmp') 15 | }); 16 | } 17 | return projects[ config.name ]; 18 | }; 19 | 20 | module.exports.Project = Project; 21 | module.exports.MirrorRepository = require('./mirror-repository'); 22 | module.exports.Branch = require('./branch'); 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | whoami=$(shell whoami) 2 | pwd=$(shell pwd) 3 | 4 | run: 5 | TMP_DIR=/tmp/data DEBUG=clb-server,clb-worker,clb-repo node lib/index.js calypso.json 6 | 7 | run-prod: 8 | TMP_DIR=/home/ubuntu/data SOCKET_DIR=/tmp DEBUG=clb-server,clb-worker,clb-repo pm2 start --name=calypso ./lib/index.js -- ./calypso.json 9 | 10 | docker-build: 11 | docker build -t clb . 12 | 13 | docker-run: docker-build 14 | mkdir -p ./tmp 15 | -docker rm clb-test 16 | docker run -it --name clb-test -v $(pwd)/tmp:/data -p 3000:3000 clb 17 | 18 | docker-run-daemon: docker-build 19 | mkdir -p ./tmp 20 | -docker stop clb-test 21 | -docker rm clb-test 22 | docker run -d --name clb-test -v $(pwd)/tmp:/data -p 3000:3000 clb 23 | 24 | docker-flush: 25 | -docker stop $(shell docker ps -a -q) 26 | -docker rm $(shell docker ps -a -q) 27 | 28 | docker-flush-all: docker-flush 29 | -docker rmi $(shell docker images -a -q) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calypso-live-branches", 3 | "version": "1.0.0", 4 | "description": "A server which checkouts and runs versions (branch) of your application on demand", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Automattic/calypso-live-branches.git" 12 | }, 13 | "author": "Automattic", 14 | "license": "GPL-2.0", 15 | "bugs": { 16 | "url": "https://github.com/Automattic/calypso-live-branches/issues" 17 | }, 18 | "homepage": "https://github.com/Automattic/calypso-live-branches#readme", 19 | "dependencies": { 20 | "ansi-to-html": "^0.4.1", 21 | "async": "^1.5.2", 22 | "cluster-hub": "^0.1.1", 23 | "cookie-session": "^1.2.0", 24 | "debug": "^2.2.0", 25 | "express": "^4.13.3", 26 | "http-proxy": "^1.12.0", 27 | "lodash": "^3.10.1", 28 | "mkdirp": "^0.5.1", 29 | "pug": "^0.1.0", 30 | "shell-escape": "^0.2.0", 31 | "ssh-url": "^0.1.5", 32 | "terminate": "^2.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:wheezy 2 | 3 | MAINTAINER Automattic 4 | 5 | WORKDIR /calypso-live-branches 6 | 7 | RUN apt-get -y update && apt-get -y install \ 8 | wget \ 9 | git \ 10 | python \ 11 | make \ 12 | build-essential 13 | 14 | ENV NODE_VERSION 4.2.3 15 | 16 | RUN wget https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz && \ 17 | tar -zxf node-v$NODE_VERSION-linux-x64.tar.gz -C /usr/local && \ 18 | ln -sf node-v$NODE_VERSION-linux-x64 /usr/local/node && \ 19 | ln -sf /usr/local/node/bin/npm /usr/local/bin/ && \ 20 | ln -sf /usr/local/node/bin/node /usr/local/bin/ && \ 21 | rm node-v$NODE_VERSION-linux-x64.tar.gz 22 | 23 | # Install base npm packages to take advantage of the docker cache 24 | COPY ./package.json /calypso-live-branches/package.json 25 | RUN npm install --production 26 | 27 | COPY . /calypso-live-branches 28 | 29 | # Change ownership 30 | RUN chown -R nobody /calypso-live-branches 31 | 32 | VOLUME [ "/data" ] 33 | EXPOSE 3000 34 | 35 | #USER nobody 36 | ENV TMP_DIR /data 37 | ENV DEBUG server,worker,branch-manager 38 | CMD node lib/index.js calypso.json 39 | -------------------------------------------------------------------------------- /lib/ws-app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var async = require('async'); 3 | 4 | function WsApp() { 5 | if (!(this instanceof WsApp)) { 6 | return new WsApp(); 7 | } 8 | this.connectMiddlewares = []; 9 | this.socketMiddlewares = []; 10 | } 11 | 12 | WsApp.prototype.useExpressMiddleware = function(middleware) { 13 | if (!this.expressApp) { 14 | this.expressApp = express(); 15 | } 16 | if (middleware === 'init') { 17 | middleware = require('express/lib/middleware/init').init(this.expressApp); 18 | } else if (middleware === 'query') { 19 | middleware = require('express/lib/middleware/query')(this.expressApp.get('query parser fn')); 20 | } 21 | this.useConnectMiddleware( middleware ); 22 | }; 23 | 24 | WsApp.prototype.useConnectMiddleware = function(middleware) { 25 | this.connectMiddlewares.push(middleware); 26 | }; 27 | 28 | WsApp.prototype.use = function(middleware) { 29 | this.socketMiddlewares.push(middleware); 30 | }; 31 | 32 | WsApp.prototype.chainMiddlewares = function(next, onError) { 33 | var connectMiddlewares = this.connectMiddlewares; 34 | return function(req, socket, head) { 35 | var resMock = { 36 | headers: {}, 37 | setHeader: function (key, value) { 38 | this.headers[key] = value; 39 | } 40 | }; 41 | async.series( 42 | connectMiddlewares.map(function(middleware) { 43 | return middleware.bind(null, req, resMock); 44 | }), 45 | function(err) { 46 | if (err) { 47 | return onError && onError(err, req, socket, head); 48 | } 49 | next(req, socket, head); 50 | } 51 | ); 52 | }; 53 | }; 54 | 55 | module.exports = WsApp; 56 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var path = require('path'); 3 | var express = require('express'); 4 | var cookieSession = require('cookie-session'); 5 | var WsApp = require('./ws-app'); 6 | var debug = require('debug')('clb-server'); 7 | 8 | module.exports = function(config) { 9 | var appDefaultConfig = config.env.CALYPSO_LIVE_DEFAULT_CONFIG && JSON.parse(config.env.CALYPSO_LIVE_DEFAULT_CONFIG) || {}; 10 | var project = require('./project')(config); 11 | var proxyManager = require('./proxy-manager')(project, appDefaultConfig); 12 | var proxyMiddlewares = require('./proxy-middlewares')(proxyManager, appDefaultConfig); 13 | var PORT = config.port || process.env.PORT || 3000; 14 | 15 | var sessionMiddleware = cookieSession({ 16 | name: 'livebranches', 17 | keys: ['key1', 'key2'] 18 | }); 19 | 20 | var app = express(); 21 | app.set('view engine', 'pug'); 22 | app.set('views', path.join(__dirname, 'views')); 23 | app.enable('trust proxy'); 24 | app.use(sessionMiddleware); 25 | app.use(proxyMiddlewares.http); 26 | 27 | var server = http.createServer(app); 28 | 29 | // Proxy WebSockets as well 30 | if (!process.env.DISABLE_WS_PROXY) { 31 | var wsApp = new WsApp(); 32 | wsApp.useExpressMiddleware('init'); 33 | wsApp.useExpressMiddleware('query'); 34 | wsApp.useConnectMiddleware(sessionMiddleware); 35 | server.on('upgrade', 36 | wsApp.chainMiddlewares( 37 | proxyMiddlewares.websocket, 38 | function handleWsError(err, req, socket, head) { 39 | debug('Error opening session', err); 40 | socket.end('Error opening session: ' + (err.message || err.toString())); 41 | } 42 | ) 43 | ); 44 | } 45 | 46 | server.listen(PORT); 47 | server.on('listening', function() { 48 | console.log('Server running on port '+PORT); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var url = require('url'); 3 | var sshUrl = require('ssh-url'); 4 | var path = require('path'); 5 | var cluster = require('cluster'); 6 | var mkdirp = require('mkdirp'); 7 | 8 | var PROTOCOL_DEFAULT = process.env.PROTOCOL_DEFAULT || 'https'; 9 | 10 | function uniqueProjectName(projectPath) { 11 | var parsedUrl = url.parse(projectPath); 12 | if(parsedUrl.host === 'github.com') { 13 | return 'gh-'+path.basename(parsedUrl.pathname, '.git'); 14 | } 15 | return path.dirname(projectPath); 16 | } 17 | 18 | function fixUrl(repoUrl) { 19 | // if no protocol is provided in the url, assume it's an ssh url 20 | // transform git@github.com:User/project.git into git+ssh://git@github.com/User/project.git 21 | if(!repoUrl.match(/^\w{3,5}(\+\w{3,5})?:\/\//)) { 22 | var sshParsed = sshUrl.parse(repoUrl); 23 | return url.format({ 24 | protocol: 'git+ssh:', 25 | hostname: sshParsed.hostname, 26 | pathname: sshParsed.pathname, 27 | auth: sshParsed.user, 28 | slashes: true 29 | }); 30 | } 31 | // replace git+https? protocols by https 32 | repoUrl = repoUrl.replace(/git\+https?:\/\//, PROTOCOL_DEFAULT + '://'); 33 | return repoUrl; 34 | } 35 | 36 | (function boot() { 37 | var argument = process.argv.slice(2)[0]; 38 | var config; 39 | 40 | if (!argument) { 41 | console.error('Enter url of repository or path to config file'); 42 | return; 43 | } 44 | 45 | try { 46 | fs.accessSync(argument); 47 | } catch (e) { 48 | config = { 49 | repository: { 50 | type: 'git', 51 | url: argument 52 | } 53 | }; 54 | } 55 | 56 | if (!config) { 57 | config = JSON.parse(fs.readFileSync(argument)); 58 | } 59 | 60 | config.repository.url = fixUrl(config.repository.url); 61 | 62 | if (!config.name) { 63 | config.name = uniqueProjectName(config.repository.url); 64 | } 65 | 66 | if(cluster.isMaster) { 67 | require('./server')(config); 68 | } else { 69 | require('./worker')(config); 70 | } 71 | 72 | })(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | This project has been deprecated and [replaced by DServe](https://github.com/Automattic/dserve). Check that out instead. 4 | 5 | # Calypso Live Branches 6 | 7 | A proxy server which checkouts a branch of your web application and runs it on demand. 8 | 9 | ## Install 10 | Clone this repository and install the dependencies: 11 | ``` 12 | git clone https://github.com/Automattic/calypso-live-branches.git 13 | cd calypso-live-branches 14 | npm install 15 | ``` 16 | 17 | 18 | ## Generic Usage 19 | 20 | Run your app with `node lib/index.js `. 21 | 22 | If your `packages.json` has all the information to build and run your app chances are it might just work. Otherwise you can create a new JSON file whose config will overwrite your `package.json` and run it with: 23 | 24 | ``` 25 | node lib/index.js my-config.json 26 | ``` 27 | 28 | For instance in [Calypso](https://github.com/Automattic/wp-calypso) we use `make build` to build our app and since it itself calls `npm install` we cannot use the default `preinstall` or `postinstall` hooks. So `calypso-live-branches` looks for the special `scripts.build` attribute. See [`calypso.json`](https://github.com/Automattic/calypso-live-branches/blob/master/calypso.json) for an exemple of configuration. 29 | 30 | Finally, use the `watchDirs` option if you want to avoid restarting your app on each change. 31 | 32 | ## Usage for Calypso 33 | 34 | Run it with `make run` 35 | 36 | ## TODO 37 | 38 | - [x] Display a page while instance is installing. 39 | - [x] Remove application specific code in `worker.js` (ie `make build` and `require('build/bundle-development.js');`). 40 | - [x] Monitor workers: restart failed workers (or mark them as failing for this commit), shutdown unused workers. 41 | - [x] Create a Dockerfile. 42 | - [x] Handle erroring branches. 43 | - [x] Report errors. 44 | - [ ] Shutdown unused branches after some time. 45 | - [ ] Add unit tests. 46 | - [ ] Make a cli and publish it as an npm package. 47 | - [ ] Find alternatives to `require` to launch the server with the patch on `net.Server.listen` (needed so we can proxy it); have a look at [`node-sandboxed-module`](https://github.com/felixge/node-sandboxed-module). 48 | -------------------------------------------------------------------------------- /lib/project/project.js: -------------------------------------------------------------------------------- 1 | var path = require( 'path' ); 2 | var fs = require( 'fs' ); 3 | var cluster = require( 'cluster' ); 4 | var util = require( 'util' ); 5 | var execSync = require( 'child_process' ).execSync; 6 | var exec = require( 'child_process' ).exec; 7 | var shellescape = require( 'shell-escape' ); 8 | var mkdirp = require( 'mkdirp' ); 9 | var debug = require( 'debug' )( 'clb-project' ); 10 | 11 | var Branch = require( './branch' ); 12 | var MirrorRepository = require( './mirror-repository' ); 13 | 14 | function createRepository( config, destination ) { 15 | if ( config.type === 'git' ) { 16 | return new MirrorRepository( config.url, path.join( destination, 'repo' ) ); 17 | } 18 | } 19 | 20 | function Project(config) { 21 | this.config = config; 22 | this.name = config.name; 23 | this.destination = path.resolve(config.destination, this.name); 24 | this.graveyard = path.join( process.env.TMP_DIR || '/tmp', 'clb-to-remove', this.name ); 25 | this.repository = createRepository( config.repository, this.destination ); 26 | this.branches = {}; 27 | this.startJanitor(); 28 | } 29 | 30 | Project.prototype.getDirectory = function() { 31 | return this.destination; 32 | }; 33 | 34 | Project.prototype.getBranch = function( branchName ) { 35 | if( ! this.branches[ branchName ] ) { 36 | this.branches[ branchName ] = new Branch( { 37 | repository: this.repository, 38 | destination: path.join( this.destination, 'branches' ), 39 | name: branchName 40 | } ); 41 | } 42 | return this.branches[ branchName ]; 43 | }; 44 | 45 | Project.prototype.getSocketPath = function( branchName ) { 46 | if ( process.env.SOCKET_DIR ) { 47 | return path.join( process.env.SOCKET_DIR, this.name, branchName + '.sock' ); 48 | } 49 | 50 | return path.join( this.destination, 'sockets', branchName+'.sock' ); 51 | }; 52 | 53 | Project.prototype.getLogPath = function( branchName ) { 54 | return path.join( this.destination, 'logs', branchName+'.log' ); 55 | }; 56 | 57 | Project.prototype.cleanupBranch = function( branchName ) { 58 | var branch = this.branches[ branchName ]; 59 | if ( ! branch ) { 60 | return new Error( 'Branch already cleaned up' ); 61 | } 62 | var graveDestination = path.join( this.graveyard, branch.name ); 63 | try { 64 | // need to be sync so we don't recreate this branch in the meantime 65 | execSync( shellescape( [ 'mkdir', '-p', path.dirname( graveDestination ) ] ) ); 66 | execSync( shellescape( [ 'mv', branch.getDirectory(), graveDestination ] ) ); 67 | if ( fs.existsSync( branch.getDirectory() ) ) { 68 | debug( 'Failed cleaning up branch ' + branch.name ); 69 | } 70 | } catch( err ) { 71 | return err; 72 | } 73 | delete this.branches[ branchName ]; 74 | return true; 75 | }; 76 | 77 | Project.prototype.startJanitor = function() { 78 | var self = this; 79 | var emptyDir = path.join( process.env.TMP_DIR || '/tmp', 'empty-dir' ); 80 | exec( 'mkdir -p ' + shellescape( [ emptyDir ] ), function() { 81 | debug( 'Starting janitor on ' + self.graveyard ); 82 | exec( 'rsync -aqr --delete --ignore-errors ' + 83 | shellescape( [ emptyDir+'/', self.graveyard+'/' ] ), 84 | function(error, stdout, stderr) { 85 | if ( error || stderr ) { 86 | debug( error || stderr ); 87 | } 88 | debug( 'Janitor stopped.' ); 89 | setTimeout( function () { 90 | self.startJanitor(); 91 | }, 60 * 1000 ); 92 | } 93 | ); 94 | } ); 95 | }; 96 | 97 | module.exports = Project; 98 | -------------------------------------------------------------------------------- /lib/views/boot.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title= message 4 | style. 5 | body { 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen-Sans", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif; 7 | background: #091e25; 8 | color: #ffffff; 9 | margin: 0; 10 | } 11 | 12 | p, 13 | .calypsolive-message { 14 | position: fixed; 15 | width: 100%; 16 | box-sizing: border-box; 17 | min-height: 55px; 18 | max-height: 80px; 19 | overflow: hidden; 20 | padding: 16px 20px; 21 | margin: 0; 22 | background: #2e4453; 23 | color: #ffffff; 24 | 25 | animation: progress-bar-animation 3300ms infinite linear; 26 | background-image: linear-gradient( -45deg, #3d596d 28%, #334b5c 28%, #334b5c 72%, #3d596d 72%); 27 | background-size: 200px 100%; 28 | background-repeat: repeat-x; 29 | background-position: 0 50px; 30 | } 31 | 32 | .calypsolive-toolbar { 33 | position: absolute; 34 | top: 12px; 35 | right: 4px; 36 | box-sizing: border-box; 37 | display: flex; 38 | } 39 | .calypsolive-toolbar a { 40 | display: inline-block; 41 | border: 1px solid #ffffff; 42 | border-radius: 3px; 43 | padding: 4px 6px; 44 | margin: 0 10px 0 0; 45 | font-size: 12px; 46 | text-decoration: none; 47 | color: #ffffff; 48 | background: #2e4453; 49 | transition: all 200ms ease-in; 50 | } 51 | .calypsolive-toolbar a:hover { 52 | background: #ffffff; 53 | color: #2e4453; 54 | } 55 | 56 | iframe, 57 | .calypsolive-log-frame { 58 | box-sizing: border-box; 59 | width: 100%; 60 | height: calc( 100% - 66px ); 61 | border: 0; 62 | padding: 66px 20px 10px 20px; 63 | } 64 | @keyframes progress-bar-animation { 65 | 0% { background-position: 400px 50px; } 66 | 100% { } 67 | } 68 | script(src="https://code.jquery.com/jquery-1.12.4.min.js", integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=", crossorigin="anonymous") 69 | script var config = !{JSON.stringify(config).replace(/<\//g, '<\\/')} 70 | script. 71 | $(function() { 72 | function isScrolledToBottom() { 73 | return (window.innerHeight + window.scrollY) >= document.body.scrollHeight; 74 | } 75 | (function refreshLog() { 76 | $.get(config.logUrl, function(logContent) { 77 | var atBottom = isScrolledToBottom(); 78 | $('.calypsolive-log-frame > pre').html(logContent); 79 | if (atBottom) { 80 | $('html, body').animate({ scrollTop: $(document).height() }, 500); 81 | } 82 | }).always(function() { 83 | setTimeout(refreshLog, 4000); 84 | }) 85 | })(); 86 | (function getStatus() { 87 | $.get(config.statusUrl, function(statusObj) { 88 | if(statusObj.status === 'DOWN') { 89 | setTimeout(function() { 90 | window.location.reload(); 91 | }, 1000); 92 | } else if(config.readyEvent && statusObj.status === config.readyEvent 93 | || !config.readyEvent && statusObj.status != 'BOOTING') { 94 | // wait 2s before refreshing 95 | setTimeout(function() { 96 | window.location.reload(); 97 | }, 4000); 98 | } else { 99 | setTimeout(getStatus, 4000); 100 | } 101 | }).fail(function() { 102 | setTimeout(getStatus, 4000); 103 | }); 104 | })(); 105 | }); 106 | body 107 | div(class="calypsolive-message") 108 | = message 109 | div(class="calypsolive-toolbar") 110 | a(href="https://github.com/Automattic/calypso-live-branches/issues") Report Issues 111 | div(class="calypsolive-log-frame") 112 | pre 113 | -------------------------------------------------------------------------------- /lib/project/branch.js: -------------------------------------------------------------------------------- 1 | var path = require( 'path' ); 2 | var url = require( 'url' ); 3 | var fs = require( 'fs' ); 4 | var cluster = require( 'cluster' ); 5 | var exec = require( 'child_process' ).exec; 6 | var util = require( 'util' ); 7 | var shellescape = require( 'shell-escape' ); 8 | 9 | var mkdirp = require( 'mkdirp' ); 10 | var debug = require( 'debug' )( 'clb-branch' ); 11 | 12 | function isValidBranchName( branchName ) { 13 | // .. is forbidden 14 | return branchName && ! branchName.match( /\.\./ ); 15 | } 16 | 17 | function Branch( config ) { 18 | this.repository = config.repository; 19 | this.destination = config.destination; 20 | this.name = config.name; 21 | if( ! isValidBranchName( this.name ) ) { 22 | throw new Error( 'Invalid branch name' ); 23 | } 24 | } 25 | 26 | Branch.prototype.exec = function( command, options, callback ) { 27 | var branchName = this.name; 28 | var escapedCommand = Array.isArray( command ) ? shellescape( command ) : command; 29 | debug( branchName+' > '+command ); 30 | if ( ! callback && typeof options === 'function' ) { 31 | callback = options; 32 | options = {}; 33 | } 34 | options = Object.assign( { 35 | timeout: 30 * 1000, 36 | killSignal: 'SIGTERM', 37 | }, options ); 38 | if ( options.cwd && ! fs.existsSync( options.cwd ) ) { 39 | return callback( new Error( 'Branch directory ' + options.cwd + ' does not exist while trying to run: ' + command ) ); 40 | } 41 | return exec( escapedCommand, options, function( err, stdout, stderr ) { 42 | debug( branchName + ' > ' + ( err || stderr.toString() || stdout.toString() ) ); 43 | callback && callback( err, stdout.toString().replace( /\s/gm, '' ), stderr.toString().replace( /\s/gm, '' ) ); 44 | } ); 45 | }; 46 | 47 | Branch.prototype.getDirectory = function() { 48 | return path.join( this.destination, this.name ); 49 | }; 50 | 51 | Branch.prototype.checkout = function( callback, currentProcessUpdate ) { 52 | var branch = this; 53 | var branchName = this.name; 54 | var destinationDir = this.getDirectory(); 55 | var repo = this.repository; 56 | var currentProcess = null; 57 | currentProcessUpdate = currentProcessUpdate || function noop() {}; 58 | currentProcess = this.exists( function( err, branchExist ) { 59 | if ( err || ! branchExist ) { 60 | return callback( err || new Error( 'Branch not found ' + branchName ) ); 61 | } 62 | currentProcessUpdate( null ); 63 | // check if branch exists remotely 64 | fs.stat(destinationDir, function( err ) { 65 | if ( ! err ) return branch.update(callback); 66 | // ensure `git clone` won't fail if branch contains slashes '/' 67 | mkdirp(destinationDir, function( err ) { 68 | if ( err ) return callback( err, destinationDir); 69 | currentProcess = branch.exec( [ 'git', 'clone', repo.getDirectory(), destinationDir ], function( err ) { 70 | if ( err ) return callback( err, destinationDir ); 71 | currentProcess = branch.exec( [ 'git', 'checkout', branch.name ], { 72 | cwd: destinationDir 73 | }, function( err ) { 74 | currentProcessUpdate( null ); 75 | callback( err, destinationDir ); 76 | } ); 77 | currentProcessUpdate( currentProcess ); 78 | } ); 79 | currentProcessUpdate( currentProcess ); 80 | } ); 81 | } ); 82 | } ); 83 | currentProcessUpdate( currentProcess ); 84 | }; 85 | 86 | Branch.prototype.update = function( callback ) { 87 | var branch = this; 88 | return branch.exec( 'git fetch origin; git reset --hard @{u}', { 89 | cwd: branch.getDirectory() 90 | }, function( err ) { 91 | callback( err, branch.getDirectory() ); 92 | } ); 93 | }; 94 | 95 | Branch.prototype.isUpToDate = function(callback) { 96 | var branch = this; 97 | return branch.exec( 'git fetch origin > /dev/null; if [ $(git rev-parse @) != $(git rev-parse @{u}) ]; then echo "not up to date"; fi;', { 98 | cwd: branch.getDirectory() 99 | }, function( err, stdout ) { 100 | callback( err, stdout.length === 0 ); 101 | } ); 102 | }; 103 | 104 | Branch.prototype.getLastCommit = function( callback ) { 105 | var branch = this; 106 | return branch.exec( 'git rev-parse HEAD', { 107 | cwd: this.getDirectory() 108 | }, function( err, stdout ) { 109 | callback( err, stdout ); 110 | } ); 111 | }; 112 | 113 | Branch.prototype.hasChanged = function( sinceCommit, inDirs, callback ) { 114 | var branch = this; 115 | if( ! inDirs || inDirs.length === 0 ) { 116 | inDirs = [ '.' ]; 117 | } 118 | 119 | return branch.exec( [ 'git', 'diff', '--name-only', sinceCommit+'..HEAD' ].concat( inDirs ), { 120 | cwd: this.getDirectory() 121 | }, function( err, stdout ) { 122 | callback( err, stdout.length > 0 ); 123 | } ); 124 | }; 125 | 126 | Branch.prototype.exists = function( callback ) { 127 | var branch = this; 128 | var repo = this.repository; 129 | // check if ref exists in the mirror repository 130 | // Do not use `origin` as we might want to know without being in a git repository 131 | return branch.exec( [ 'git', 'ls-remote', repo.getDirectory(), branch.name ], function( err, stdout ) { 132 | var branchExist = stdout.length > 0; 133 | callback(err, branchExist); 134 | } ); 135 | }; 136 | 137 | module.exports = Branch; 138 | -------------------------------------------------------------------------------- /lib/proxy-middlewares.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var debug = require('debug')('clb-server'); 3 | var merge = require('lodash').merge; 4 | var AnsiToHTML = require('ansi-to-html'); 5 | var ansiToHTMLConverter = new AnsiToHTML(); 6 | 7 | function escapeRegExp(text) { 8 | return text.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 9 | } 10 | 11 | var subdomainRegexp = null; 12 | if(process.env.HOST) { 13 | subdomainRegexp = new RegExp("(.+?)" + escapeRegExp('.'+process.env.HOST)); 14 | } 15 | 16 | function getBranchName(req) { 17 | var branchName; 18 | if(subdomainRegexp) { 19 | var matches = req.hostname.match(subdomainRegexp); 20 | if(matches && matches[1]) { 21 | branchName = matches[1].replace('.', '/'); 22 | } 23 | } 24 | if(!branchName) { 25 | branchName = req.query.branch || req.session.branch || 'master'; 26 | } 27 | return branchName; 28 | } 29 | 30 | function serveBootPage(req, res, message, options) { 31 | if(res.headersSent) return; 32 | options = options || {}; 33 | res.status(202); 34 | if(isHTML(req)) { 35 | res.render('boot', { 36 | message: message, 37 | config: { 38 | logUrl: req.path + '?branch=' + getBranchName(req) + '&log=1', 39 | statusUrl: req.path + '?branch=' + getBranchName(req) + '&status=1', 40 | readyEvent: options.readyEvent 41 | } 42 | } ); 43 | } else { 44 | res.send(message); 45 | } 46 | } 47 | 48 | function serveLogFile(req, res, logPath, options) { 49 | options = options || {}; 50 | var readStream = fs.createReadStream(logPath, { encoding: 'utf8' }); 51 | readStream.on('error', function(err) { 52 | res.status(500).end(err.toString()); 53 | }); 54 | if(options.format === 'html') { 55 | readStream.on('data', function(chunk) { 56 | res.write(ansiToHTMLConverter.toHtml(chunk)); 57 | }); 58 | readStream.on('end', function() { 59 | res.end(); 60 | }); 61 | } else { 62 | readStream.pipe(res); 63 | } 64 | } 65 | 66 | function isHTML(req) { 67 | var acceptedContentTypes = (req.header('accept') || '').split(";")[0]; 68 | if(acceptedContentTypes) { 69 | acceptedContentTypes = acceptedContentTypes.split(','); 70 | if(acceptedContentTypes.indexOf('text/html') >= 0) return true; 71 | } 72 | return false; 73 | } 74 | 75 | module.exports = function(proxyManager, appDefaultConfig) { 76 | 77 | function proxyHTTP(req, res, next) { 78 | if ('list_branches' in req.query) { 79 | return res.json( proxyManager.listBranches() ); 80 | } 81 | var branchName = getBranchName(req); 82 | var options = merge({}, appDefaultConfig, req.query); 83 | if ('log' in req.query) { 84 | var branchOutput = proxyManager.project.getLogPath(branchName); 85 | return serveLogFile(req, res, branchOutput, options); 86 | } 87 | if ('reboot' in req.query) { 88 | proxyManager.stopServingBranch(branchName, true); 89 | return res.redirect(req.path + '?branch=' + branchName); 90 | } 91 | if ('status' in req.query) { 92 | return proxyManager.checkUpdated(branchName, function() { // ignore update errors 93 | proxyManager.getCommitId(branchName, function(err, commitId) { 94 | res.send({ 95 | branch: branchName, 96 | status: proxyManager.getStatus(branchName), 97 | commit: !err && commitId 98 | }); 99 | }); 100 | }); 101 | } 102 | // update branch in session 103 | if (branchName !== req.session.branch) { 104 | req.session.branch = branchName; 105 | } 106 | // boot if branch is not up 107 | if (!proxyManager.isUp(branchName)) { 108 | if ( proxyManager.hasErrored( branchName ) ) { 109 | serveBootPage(req, res, 'Branch has encountered an error. Retrying in 20s', options); 110 | return; 111 | } 112 | debug('Booting branch', branchName); 113 | proxyManager.bootBranch(branchName, function(err) { 114 | if(err) { 115 | debug(err); 116 | serveBootPage(req, res, err.message, options); 117 | } 118 | }); 119 | // wait 5 seconds before serving the booting page so `proxyManager.bootBranch` may print 120 | // an error if it fails quickly. 121 | setTimeout(function() { 122 | serveBootPage(req, res, 'Booting branch...', options); 123 | }, 5 * 1000); 124 | return; 125 | } 126 | if (proxyManager.isBooting(branchName)) { 127 | serveBootPage(req, res, 'Booting branch...', options); 128 | } else { 129 | proxyManager.checkUpdated(branchName, function(err, mustRestart) { 130 | if (err) { 131 | return serveBootPage(req, res, err.message, options); 132 | } 133 | if (mustRestart) { 134 | return serveBootPage(req, res, 'Rebooting branch...', options); 135 | } 136 | proxyManager.proxyRequest(branchName, req, res, function(err) { 137 | if ( err ) { 138 | debug( 'Proxying request to ' + branchName + ' failed.', err ); 139 | // retry on error 140 | return serveBootPage(req, res, 'Rebooting branch...', options); 141 | } 142 | } ); 143 | }); 144 | } 145 | } 146 | 147 | // assumes the branch has booted 148 | function proxyWebsocket(req, socket, head) { 149 | var branchName = getBranchName(req); 150 | if (!branchName || !proxyManager.proxies[branchName]) { 151 | socket.end('Session not found'); 152 | return; 153 | } 154 | var proxy = proxyManager.proxies[branchName]; 155 | if (!proxy) { 156 | socket.end('Proxy not found'); 157 | return; 158 | } 159 | proxy.ws(req, socket, head, function(err) { 160 | debug(err); 161 | }); 162 | } 163 | 164 | return { 165 | http: proxyHTTP, 166 | websocket: proxyWebsocket 167 | }; 168 | }; 169 | -------------------------------------------------------------------------------- /lib/project/mirror-repository.js: -------------------------------------------------------------------------------- 1 | var url = require( 'url' ); 2 | var fs = require( 'fs' ); 3 | var cluster = require( 'cluster' ); 4 | var exec = require( 'child_process' ).exec; 5 | var util = require( 'util' ); 6 | var EventEmitter = require( 'events' ); 7 | var shellescape = require( 'shell-escape' ); 8 | var mkdirp = require( 'mkdirp' ); 9 | var isEqual = require( 'lodash' ).isEqual; 10 | var debug = require( 'debug' )( 'clb-repo' ); 11 | 12 | function MirrorRepository( url, destination ) { 13 | EventEmitter.call( this ); 14 | this.remoteUrl = url; 15 | this.destination = destination; 16 | this.ready = false; 17 | this.initializing = false; 18 | this.syncing = false; 19 | } 20 | 21 | util.inherits( MirrorRepository, EventEmitter ); 22 | 23 | MirrorRepository.prototype.getDirectory = function() { 24 | return this.destination; 25 | }; 26 | 27 | MirrorRepository.prototype.init = function( callback ) { 28 | var repo = this; 29 | var repoDir = this.getDirectory(); 30 | if ( repo.initializing ) { 31 | return repo.once( 'ready', callback ); 32 | } 33 | repo.initializing = true; 34 | // check if repo exists 35 | fs.stat( repoDir, function( err ) { 36 | if ( ! err ) { 37 | repo.ready = true; 38 | repo.initializing = false; 39 | repo.emit( 'ready' ); 40 | return callback(); 41 | } 42 | mkdirp( repoDir, function() { 43 | debug( 'Cloning ' + repo.remoteUrl + ' to ' + repoDir ); 44 | exec( 'git clone --mirror ' + repo.remoteUrl + ' ' + repoDir, function( err ) { 45 | if ( err ) return callback( err ); 46 | debug( 'Updating mirror git repository configuration' ); 47 | repo.updateConfig( function( err ) { 48 | if ( err ) return callback( err ); 49 | debug( 'Mirror repository is ready' ); 50 | repo.ready = true; 51 | repo.initializing = false; 52 | repo.emit( 'ready' ); 53 | callback( err ); 54 | }); 55 | }); 56 | }); 57 | }); 58 | }; 59 | 60 | MirrorRepository.prototype.updateConfig = function( callback ) { 61 | if ( process.env.ENABLE_GH_PULL ) { 62 | exec( 'git config --add remote.origin.fetch "+refs/pull/*/head:refs/heads/gh-pull/*"; git fetch origin', { 63 | cwd: this.getDirectory() 64 | }, callback ); 65 | } else { 66 | callback(); 67 | } 68 | }; 69 | 70 | /* 71 | * To ensure that a git command is only executed once at a time on the mirror repository, 72 | * any commands on a MirrorRepository instance must be called from the master process 73 | */ 74 | MirrorRepository.prototype.waitAvailable = function( callback ) { 75 | var repo = this; 76 | repo._checkReady( function( err ) { 77 | if ( err ) debug( err ); 78 | repo._checkNotSyncing( callback ); 79 | } ); 80 | }; 81 | 82 | MirrorRepository.prototype._checkReady = function( callback ) { 83 | if ( ! this.ready && ! this.initializing ) { 84 | return this.init( callback ); 85 | } 86 | if ( ! this.ready && this.initializing ) { 87 | return this.once( 'ready', callback ); 88 | } 89 | callback(); 90 | }; 91 | 92 | MirrorRepository.prototype._checkNotSyncing = function( callback ) { 93 | if( this.syncing ) { 94 | return this.once( 'synced', callback ); 95 | } 96 | callback(); 97 | }; 98 | 99 | MirrorRepository.prototype.keepSynced = function( options ) { 100 | var repo = this; 101 | this.init( function( err ) { 102 | if ( err ) console.error( err ); 103 | var currentBranches = []; 104 | ( function sync() { 105 | repo.syncing = true; 106 | repo.emit( 'syncing' ); 107 | repo._sync( function( err ) { 108 | if ( err ) console.error( 'Ignoring sync error: ', err ); 109 | repo.syncing = false; 110 | repo.emit( 'synced' ); 111 | repo.listBranches( options.maxBranches, function( err, branches ) { 112 | if ( err ) console.error( err ); 113 | branches = branches || []; 114 | branches.sort(); 115 | if( ! isEqual( currentBranches, branches ) ) { 116 | currentBranches = branches; 117 | repo.emit( 'update', branches ); 118 | } 119 | setTimeout( sync, 60 * 1000 ); 120 | } ); 121 | } ); 122 | } )(); 123 | } ); 124 | }; 125 | 126 | MirrorRepository.prototype.exec = function( command, callback ) { 127 | debug( 'REPO > ' + command ); 128 | var directory = this.getDirectory(); 129 | if ( ! fs.existsSync( directory ) ) { 130 | return callback( new Error( 'Repository directory ' + directory + ' does not exist while trying to run: ' + command ) ); 131 | } 132 | exec( command, { 133 | cwd: directory 134 | }, function( err, stdout, stderr ) { 135 | debug( 'REPO > ' + ( err || stderr.toString() || stdout.toString() ) ); 136 | callback && callback( err, stdout.toString(), stderr.toString() ); 137 | } ); 138 | }; 139 | 140 | MirrorRepository.prototype._sync = function( callback ) { 141 | this.exec( 'git fetch -p origin', function( err, stdout ) { 142 | callback( err ); 143 | } ); 144 | }; 145 | 146 | MirrorRepository.prototype.listBranches = function( maxBranches, callback ) { 147 | var cmd; 148 | if ( ! callback ) { 149 | callback = maxBranches; 150 | cmd = 'git branch --list --no-color'; 151 | } else { 152 | cmd = 'git branch --list --no-color --sort=-committerdate | head -n ' + shellescape( [ maxBranches ] ); 153 | } 154 | this.exec( cmd, function( err, stdout ) { 155 | if ( err ) return callback( err ); 156 | var branches = stdout.split( '\n' ).map( function( line ) { 157 | return line.replace( /\s|\*/g, '' ); 158 | } ).filter( function( line ) { // remove empty lines 159 | return !!line; 160 | } ); 161 | callback( null, branches ); 162 | } ); 163 | }; 164 | 165 | module.exports = MirrorRepository; 166 | -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var net = require('net'); 4 | var cluster = require('cluster'); 5 | var _ = require('lodash'); 6 | var exec = require('child_process').exec; 7 | var mkdirp = require('mkdirp'); 8 | var Hub = require('cluster-hub'); 9 | var debug = require('debug')('clb-worker'); 10 | var async = require('async'); 11 | var terminate = require('terminate'); 12 | 13 | var hub = new Hub(); 14 | var worker = cluster.worker; 15 | 16 | // patch net.Server.listen to use the socket file instead of the port given by the program 17 | function patchNetServerListen(socketPath, onConnected) { 18 | var originalNetServerListen = net.Server.prototype.listen; 19 | var serverStarted = false; 20 | var calledOnce = false; 21 | net.Server.prototype.listen = function() { 22 | var server = this; 23 | var args = Array.prototype.slice.call( arguments ); 24 | var realCallback; 25 | 26 | debug('listen() called from worker %d', worker.id); 27 | // prevent binding on the same socket file multiple times 28 | if (calledOnce) { 29 | return originalNetServerListen.apply(this, arguments); 30 | } 31 | calledOnce = true; 32 | if(args.length > 0 && typeof args[args.length-1] === 'function') { 33 | realCallback = args[args.length-1]; 34 | } 35 | worker.on('exit', function() { 36 | debug('worker %d exiting, closing server', worker.id); 37 | server.close(); 38 | server.unref(); 39 | setImmediate(function() { 40 | server.emit('close'); 41 | }); 42 | }); 43 | originalNetServerListen.call(server, socketPath, function connectListener() { 44 | if(realCallback) { 45 | realCallback.apply(this, arguments); 46 | } 47 | if(!serverStarted) { 48 | serverStarted = true; 49 | onConnected && onConnected(); 50 | } 51 | }); 52 | }; 53 | } 54 | 55 | function patchPackageJSON(destination, config, callback) { 56 | var packageJSONPath = path.join(destination, 'package.json'); 57 | fs.readFile(packageJSONPath, function(err, buffer) { 58 | if(err) return callback(); // file does not exist (probably) 59 | var packageJSON = JSON.parse(buffer.toString()); 60 | // local config must overwrite packageJSON config 61 | _.merge(config, _.merge({ scripts: {} }, packageJSON, config)); 62 | fs.writeFile(packageJSONPath, JSON.stringify(config, null, 2), callback); 63 | }); 64 | } 65 | 66 | function prepareApp(destination, config, callback) { 67 | _.merge(process.env, config.env || {}); 68 | var installProcess = exec('npm install', { cwd: destination, maxBuffer: 1024 * 1024 * 5 /* 5MB */ }, function(err, stdout, stderr) { 69 | if(err) return callback(err); 70 | // reinit paths for require 71 | process.chdir(destination); 72 | require('module').Module._initPaths(); 73 | callback(); 74 | }); 75 | return installProcess; 76 | } 77 | 78 | function serializeError(err) { 79 | if(!err || typeof err === 'string') return err; 80 | return { 81 | name: err.name || 'Error', 82 | message: err.message, 83 | stack: err.stack 84 | }; 85 | } 86 | 87 | function checkBranchStillExists( branch ) { 88 | branch.exists(function(err, branchExists) { 89 | if(!branchExists) { 90 | process.exit(0); 91 | } 92 | setTimeout(function() { 93 | checkBranchStillExists( branch ); 94 | }, 60 * 60 * 1000 ); // 1h 95 | }) 96 | } 97 | 98 | function resetFile(filePath, callback) { 99 | // create dir for the file 100 | mkdirp(path.dirname(filePath), function() { 101 | // ensure the file does not exist yet 102 | fs.unlink(filePath, callback) 103 | }); 104 | } 105 | 106 | function terminateSubprocess( subprocess, subprocessName, branchName ) { 107 | if ( subprocess ) { 108 | debug( 'Terminating ' + subprocessName + ' subprocess...' ); 109 | terminate( subprocess.pid, function( err ) { 110 | if ( err ) { 111 | debug( 'Could not terminate ' + subprocessName + ' subprocess for ' + branchName ); 112 | } else { 113 | debug( 'Successfully terminated ' + subprocessName + ' subprocess for ' + branchName ); 114 | } 115 | } ); 116 | } 117 | } 118 | 119 | module.exports = function(config) { 120 | var project = require('./project')(config); 121 | var currentBranch, branchDestination; 122 | 123 | hub.on('init', function(data, sender, callback) { 124 | if(currentBranch) return callback('Branch already booted'); 125 | var branchName = data.branch; 126 | try { 127 | currentBranch = project.getBranch(branchName); 128 | } catch(err) { 129 | return callback(serializeError(err)); 130 | } 131 | 132 | var socketPath = project.getSocketPath(branchName); 133 | var logPath = project.getLogPath(branchName); 134 | debug( 'Worker booting ' + branchName ); 135 | debug( 'Clearing log ' + logPath ); 136 | resetFile(logPath, function() { 137 | debug( 'Redirecting log calls to ' + logPath ); 138 | var outputStream = fs.createWriteStream(logPath); 139 | worker.on('exit', function() { 140 | outputStream.end(); 141 | }); 142 | // patch console calls 143 | process.stdout.write = process.stderr.write = outputStream.write.bind( outputStream ); 144 | 145 | if (socketPath.length >= 103) { 146 | console.warn('WARNING: Socket path is longer than UNIX_PATH_MAX on OS X (104), this might cause some problems'); 147 | } 148 | 149 | debug( 'Checking out branch ' + branchName ); 150 | var checkoutCurrentProcess = null; 151 | currentBranch.checkout(function(err, destination) { 152 | if(err) return callback(serializeError(err)); 153 | 154 | branchDestination = destination; 155 | console.log('Switched to branch '+branchName+' at '+destination); 156 | 157 | debug( 'Patching package.json for branch ' + branchName ); 158 | patchPackageJSON(destination, config, function(err) { 159 | if(err) return callback(serializeError(err)); 160 | debug( 'Preparing branch (npm install) ' + branchName ); 161 | console.log('Installing...'); 162 | // run application in this context by 163 | // - overwriting the ENV 164 | // - using package.json + config to build the app 165 | // - calling the main script with require (warning: the test (require.main === module) won't pass) 166 | var installProcess = prepareApp(destination, config, function(err) { 167 | if(err) return callback(serializeError(err)); 168 | installProcess = null; 169 | // ensure the socket file does not exist yet 170 | resetFile(socketPath, function() { 171 | debug( 'Patching net.Server.listen for branch ' + branchName ); 172 | patchNetServerListen(socketPath, callback); 173 | console.log('net.Server.prototype.listen patched'); 174 | require(path.join(destination, config.main || 'index')); 175 | }); 176 | }); 177 | installProcess.stdout.on('data', function(chunk) { 178 | outputStream.write(chunk); 179 | }); 180 | installProcess.stderr.on('data', function(chunk) { 181 | outputStream.write(chunk); 182 | }); 183 | worker.on('exit', function() { 184 | terminateSubprocess( installProcess, '`npm install`', branchName ); 185 | }); 186 | }); 187 | }, function currentProcessUpdate( currentProcess ) { 188 | checkoutCurrentProcess = currentProcess; 189 | } ); 190 | worker.on( 'exit', function() { 191 | terminateSubprocess( checkoutCurrentProcess, 'checkout', branchName ); 192 | } ); 193 | }); 194 | }); 195 | 196 | hub.on('update', function(data, sender, callback) { 197 | currentBranch.isUpToDate(function(err, upToDate) { 198 | if(err) return callback(serializeError(err)); 199 | if(upToDate) return callback(null, false); 200 | currentBranch.getLastCommit(function(err, lastCommit) { 201 | if(err) return callback(serializeError(err)); 202 | currentBranch.update(function(err) { 203 | if(err) return callback(serializeError(err)); 204 | currentBranch.hasChanged(lastCommit, config.watchPaths, function(err, changed) { 205 | callback(serializeError(err), changed); 206 | }); 207 | }); 208 | }); 209 | }); 210 | }); 211 | }; 212 | -------------------------------------------------------------------------------- /lib/proxy-manager.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var net = require('net'); 4 | var http = require('http'); 5 | var cluster = require('cluster'); 6 | var child_process = require('child_process'); 7 | var os = require('os'); 8 | var async = require('async'); 9 | var mkdirp = require('mkdirp'); 10 | var httpProxy = require('http-proxy'); 11 | var debug = require('debug')('clb-server'); 12 | var Hub = require('cluster-hub'); 13 | 14 | function noop() {} 15 | 16 | function deserializeError(err) { 17 | if( ! err || typeof err !== 'object' ) return; 18 | err.constructor = Error; 19 | err.__proto__ = Error.prototype; 20 | Object.defineProperties( err, Object.keys( err ).reduce( function( acc, errProperty ) { 21 | acc[ errProperty ] = { 22 | value: err[ errProperty ], 23 | enumerable: false 24 | }; 25 | return acc; 26 | }, {} ) ); 27 | } 28 | 29 | function ProxyManager(project, calypsoLiveConfig) { 30 | if (!(this instanceof ProxyManager)) { 31 | return new ProxyManager(project, calypsoLiveConfig); 32 | } 33 | this.project = project; 34 | this.hub = new Hub(); 35 | this.workers = {}; 36 | this.proxies = {}; 37 | this.bootStatuses = {}; 38 | this.calypsoLiveConfig = calypsoLiveConfig; 39 | 40 | // start refreshing the mirror repository periodically 41 | this.project.repository.keepSynced({ 42 | maxBranches: calypsoLiveConfig.maxBranches 43 | }); 44 | this.project.repository.on('update', this.serveActiveBranches.bind(this)); 45 | } 46 | 47 | ProxyManager.prototype.serveActiveBranches = function(activeBranches) { 48 | var self = this; 49 | // always serve master 50 | if ( activeBranches.indexOf( 'master' ) !== -1 ) { 51 | activeBranches.push( 'master' ); 52 | } 53 | // kill off dead branches first 54 | for ( var branch in self.workers ) { 55 | var proxy = self.proxies[ branch ]; 56 | // a branch should be killed if it's not active (deleted or old) and if it is booted but not accessed in the last 24h 57 | if ( activeBranches.indexOf( branch ) === -1 && proxy && proxy.lastAccess < Date.now() - 24 * 3600 * 1000 ) { 58 | debug( 'Stopping inactive branch ' + branch ); 59 | this.stopServingBranch( branch ); 60 | } 61 | } 62 | if ( this.calypsoLiveConfig.autoBoot ) { 63 | async.eachLimit( activeBranches, os.cpus().length - 1 || 1, function iterator( branch, callback ) { 64 | if ( ! self.workers[ branch ] ) { 65 | self.serveBranch( branch, callback ); 66 | } 67 | }); 68 | } 69 | var branchInfo = this.listBranches(); 70 | debug( 'Active branches updated, now serving ' + branchInfo.active.length + ' branches (' + branchInfo.ready.length + ' ready).' ); 71 | }; 72 | 73 | ProxyManager.prototype.serveBranch = function(branchName, callback) { 74 | var worker = cluster.fork(); 75 | var self = this; 76 | this.workers[branchName] = worker; 77 | worker.on('exit', function() { 78 | self.stopServingBranch(branchName); 79 | }); 80 | worker.on('message', function(msg) { 81 | if (msg && msg.boot && self.isUp(branchName)) { 82 | debug('Branch '+branchName+' is ' + msg.boot); 83 | self.bootStatuses[branchName] = msg.boot; 84 | } 85 | }); 86 | this.project.repository.waitAvailable(function() { 87 | self.hub.requestWorker(worker, 'init', { 88 | branch: branchName 89 | }, function(err) { 90 | deserializeError(err); 91 | worker.lastUpdated = Date.now(); 92 | callback && callback(err); 93 | }); 94 | }); 95 | }; 96 | 97 | ProxyManager.prototype.stopServingBranch = function( branchName, cleanup, callback ) { 98 | var self = this; 99 | callback = callback || noop; 100 | if ( typeof cleanup === 'function' ) { 101 | callback = cleanup; 102 | cleanup = false; 103 | } 104 | var proxy = this.proxies[ branchName ]; 105 | if ( proxy ) { 106 | proxy.close(); 107 | delete this.proxies[ branchName ]; 108 | } 109 | this.killWorker( branchName, function() { 110 | debug( 'Worker for ' + branchName + ' is dead.' ); 111 | if ( cleanup ) { 112 | debug( 'Cleaning up ' + branchName ); 113 | self.project.cleanupBranch( branchName ); 114 | } 115 | // Don't reset the error status immediately 116 | setTimeout( function() { 117 | delete self.bootStatuses[ branchName ]; 118 | callback(); 119 | }, 20 * 1000 ); 120 | } ); 121 | }; 122 | 123 | ProxyManager.prototype.killWorker = function( branchName, callback ) { 124 | var self = this; 125 | var worker = this.workers[branchName]; 126 | if ( worker && ! worker.isDead() ) { 127 | worker.kill( 'SIGTERM' ); 128 | // Give it 2s to shutdown cleanly 129 | setTimeout( function() { 130 | if ( ! worker.isDead() || ! worker.killed ) { 131 | worker.kill( 'SIGKILL' ); 132 | setTimeout( function() { 133 | delete self.workers[ branchName ]; 134 | callback(); 135 | }, 100 ); 136 | } else { 137 | delete self.workers[ branchName ]; 138 | callback(); 139 | } 140 | }, 2000 ); 141 | } else { 142 | delete self.workers[ branchName ]; 143 | callback(); 144 | } 145 | }; 146 | 147 | ProxyManager.prototype.checkUpdated = function( branchName, callback ) { 148 | var self = this; 149 | callback = callback || noop; 150 | if ( ! this.isUp( branchName ) || this.isBooting( branchName ) ) { 151 | return callback( new Error('branch not ready' ) ); 152 | } 153 | var worker = this.workers[ branchName ]; 154 | // Don't check if recent update (30s) 155 | if ( worker.lastUpdated > Date.now() - 30 * 1000 ) { 156 | return callback( null, false ); 157 | } 158 | if( worker.updating ) { 159 | return worker.once( 'branch updated', callback ); 160 | } 161 | worker.setMaxListeners( 1000 ); 162 | worker.updating = true; 163 | this.project.repository.waitAvailable( function() { 164 | self.hub.requestWorker( worker, 'update', null, function( err, mustRestart ) { 165 | deserializeError( err ); 166 | if ( err ) { 167 | self.markErrored( branchName ); 168 | return; 169 | } 170 | worker.lastUpdated = Date.now(); 171 | if ( mustRestart ) { 172 | self.stopServingBranch( branchName, function() { 173 | callback( null, mustRestart ); 174 | } ); 175 | } else { 176 | worker.updating = false; 177 | worker.emit( 'branch updated' ); 178 | callback( null, false ); 179 | } 180 | } ); 181 | } ); 182 | }; 183 | 184 | ProxyManager.prototype.proxyRequest = function(branchName, req, res, next) { 185 | if(res.headersSent) return; 186 | var proxy = this.proxies[branchName]; 187 | if(!proxy) return next(new Error('proxy stopped')); 188 | proxy.lastAccess = Date.now(); 189 | proxy.web(req, res, next); 190 | }; 191 | 192 | ProxyManager.prototype.isUp = function( branchName ) { 193 | return ( branchName in this.workers ) && this.bootStatuses[ branchName ] !== 'ERROR'; 194 | }; 195 | 196 | ProxyManager.prototype.isBooting = function(branchName) { 197 | return this.workers[branchName] && !this.proxies[branchName]; 198 | }; 199 | 200 | ProxyManager.prototype.getStatus = function(branchName) { 201 | if (this.bootStatuses[branchName]) { 202 | return this.bootStatuses[branchName]; 203 | } else if (this.workers[branchName] && this.proxies[branchName]) { 204 | return 'UP'; 205 | } else if (this.workers[branchName]) { 206 | return 'BOOTING'; 207 | } else { 208 | return 'DOWN'; 209 | } 210 | }; 211 | 212 | ProxyManager.prototype.getCommitId = function(branchName, callback) { 213 | var currentBranch; 214 | if (!this.isUp(branchName)) { 215 | return callback(); 216 | } 217 | try { 218 | currentBranch = this.project.getBranch(branchName); 219 | } catch(err) { 220 | return callback(err); 221 | } 222 | currentBranch.getLastCommit(callback); 223 | }; 224 | 225 | ProxyManager.prototype.bootBranch = function( branchName, callback ) { 226 | var self = this; 227 | if ( this.hasErrored( branchName ) ) { 228 | return callback( new Error( branchName + ' has errored. Please wait while this branch is being cleaned up.' ) ); 229 | } 230 | this.serveBranch( branchName, function ( err ) { 231 | if ( err ) { 232 | self.markErrored( branchName ); 233 | debug( 'Branch '+branchName+' has errored. Cause: ', err ); 234 | callback( err ); 235 | return; 236 | } 237 | var socketPath = self.project.getSocketPath(branchName); 238 | debug('creating proxy to', socketPath); 239 | self.proxies[branchName] = httpProxy.createProxyServer({ 240 | target: { 241 | socketPath: socketPath 242 | } 243 | }); 244 | self.proxies[branchName].lastAccess = Date.now(); 245 | self.proxies[branchName].on('error', function(err) { 246 | debug(err); 247 | }); 248 | }); 249 | }; 250 | 251 | ProxyManager.prototype.listBranches = function() { 252 | var self = this; 253 | var activeBranches = Object.keys( this.workers ); 254 | var upBranches = Object.keys( this.proxies ); 255 | var readyBranches; 256 | if ( this.calypsoLiveConfig.readyEvent ) { 257 | readyBranches = upBranches.filter( function( branchName ) { return self.bootStatuses[branchName] === self.calypsoLiveConfig.readyEvent; } ); 258 | } else { 259 | readyBranches = upBranches; 260 | } 261 | var erroredBranches = activeBranches.filter( function( branchName ) { return self.bootStatuses[branchName] === "ERROR"; } ); 262 | return { 263 | active: activeBranches, 264 | up: upBranches, 265 | ready: readyBranches, 266 | errored: erroredBranches, 267 | } 268 | }; 269 | 270 | ProxyManager.prototype.markErrored = function( branchName ) { 271 | var self = this; 272 | if ( this.hasErrored( branchName ) ) { 273 | return; 274 | } 275 | debug( 'Branch ' + branchName + ' has errored. It will be cleaned up and ready to boot again in 20s.' ); 276 | this.bootStatuses[ branchName ] = 'ERROR'; 277 | self.stopServingBranch( branchName, true ); 278 | }; 279 | 280 | ProxyManager.prototype.hasErrored = function( branchName ) { 281 | return this.bootStatuses[ branchName ] === 'ERROR'; 282 | }; 283 | 284 | module.exports = ProxyManager; 285 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The GNU General Public License, Version 2, June 1991 (GPLv2) 2 | ============================================================ 3 | 4 | > Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | > 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license 8 | document, but changing it is not allowed. 9 | 10 | 11 | Preamble 12 | -------- 13 | 14 | The licenses for most software are designed to take away your freedom to share 15 | and change it. By contrast, the GNU General Public License is intended to 16 | guarantee your freedom to share and change free software--to make sure the 17 | software is free for all its users. This General Public License applies to most 18 | of the Free Software Foundation's software and to any other program whose 19 | authors commit to using it. (Some other Free Software Foundation software is 20 | covered by the GNU Library General Public License instead.) You can apply it to 21 | your programs, too. 22 | 23 | When we speak of free software, we are referring to freedom, not price. Our 24 | General Public Licenses are designed to make sure that you have the freedom to 25 | distribute copies of free software (and charge for this service if you wish), 26 | that you receive source code or can get it if you want it, that you can change 27 | the software or use pieces of it in new free programs; and that you know you can 28 | do these things. 29 | 30 | To protect your rights, we need to make restrictions that forbid anyone to deny 31 | you these rights or to ask you to surrender the rights. These restrictions 32 | translate to certain responsibilities for you if you distribute copies of the 33 | software, or if you modify it. 34 | 35 | For example, if you distribute copies of such a program, whether gratis or for a 36 | fee, you must give the recipients all the rights that you have. You must make 37 | sure that they, too, receive or can get the source code. And you must show them 38 | these terms so they know their rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and (2) offer 41 | you this license which gives you legal permission to copy, distribute and/or 42 | modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain that 45 | everyone understands that there is no warranty for this free software. If the 46 | software is modified by someone else and passed on, we want its recipients to 47 | know that what they have is not the original, so that any problems introduced by 48 | others will not reflect on the original authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software patents. We wish 51 | to avoid the danger that redistributors of a free program will individually 52 | obtain patent licenses, in effect making the program proprietary. To prevent 53 | this, we have made it clear that any patent must be licensed for everyone's free 54 | use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and modification 57 | follow. 58 | 59 | 60 | Terms And Conditions For Copying, Distribution And Modification 61 | --------------------------------------------------------------- 62 | 63 | **0.** This License applies to any program or other work which contains a notice 64 | placed by the copyright holder saying it may be distributed under the terms of 65 | this General Public License. The "Program", below, refers to any such program or 66 | work, and a "work based on the Program" means either the Program or any 67 | derivative work under copyright law: that is to say, a work containing the 68 | Program or a portion of it, either verbatim or with modifications and/or 69 | translated into another language. (Hereinafter, translation is included without 70 | limitation in the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not covered by 73 | this License; they are outside its scope. The act of running the Program is not 74 | restricted, and the output from the Program is covered only if its contents 75 | constitute a work based on the Program (independent of having been made by 76 | running the Program). Whether that is true depends on what the Program does. 77 | 78 | **1.** You may copy and distribute verbatim copies of the Program's source code 79 | as you receive it, in any medium, provided that you conspicuously and 80 | appropriately publish on each copy an appropriate copyright notice and 81 | disclaimer of warranty; keep intact all the notices that refer to this License 82 | and to the absence of any warranty; and give any other recipients of the Program 83 | a copy of this License along with the Program. 84 | 85 | You may charge a fee for the physical act of transferring a copy, and you may at 86 | your option offer warranty protection in exchange for a fee. 87 | 88 | **2.** You may modify your copy or copies of the Program or any portion of it, 89 | thus forming a work based on the Program, and copy and distribute such 90 | modifications or work under the terms of Section 1 above, provided that you also 91 | meet all of these conditions: 92 | 93 | * **a)** You must cause the modified files to carry prominent notices stating 94 | that you changed the files and the date of any change. 95 | 96 | * **b)** You must cause any work that you distribute or publish, that in whole 97 | or in part contains or is derived from the Program or any part thereof, to 98 | be licensed as a whole at no charge to all third parties under the terms of 99 | this License. 100 | 101 | * **c)** If the modified program normally reads commands interactively when 102 | run, you must cause it, when started running for such interactive use in the 103 | most ordinary way, to print or display an announcement including an 104 | appropriate copyright notice and a notice that there is no warranty (or 105 | else, saying that you provide a warranty) and that users may redistribute 106 | the program under these conditions, and telling the user how to view a copy 107 | of this License. (Exception: if the Program itself is interactive but does 108 | not normally print such an announcement, your work based on the Program is 109 | not required to print an announcement.) 110 | 111 | These requirements apply to the modified work as a whole. If identifiable 112 | sections of that work are not derived from the Program, and can be reasonably 113 | considered independent and separate works in themselves, then this License, and 114 | its terms, do not apply to those sections when you distribute them as separate 115 | works. But when you distribute the same sections as part of a whole which is a 116 | work based on the Program, the distribution of the whole must be on the terms of 117 | this License, whose permissions for other licensees extend to the entire whole, 118 | and thus to each and every part regardless of who wrote it. 119 | 120 | Thus, it is not the intent of this section to claim rights or contest your 121 | rights to work written entirely by you; rather, the intent is to exercise the 122 | right to control the distribution of derivative or collective works based on the 123 | Program. 124 | 125 | In addition, mere aggregation of another work not based on the Program with the 126 | Program (or with a work based on the Program) on a volume of a storage or 127 | distribution medium does not bring the other work under the scope of this 128 | License. 129 | 130 | **3.** You may copy and distribute the Program (or a work based on it, under 131 | Section 2) in object code or executable form under the terms of Sections 1 and 2 132 | above provided that you also do one of the following: 133 | 134 | * **a)** Accompany it with the complete corresponding machine-readable source 135 | code, which must be distributed under the terms of Sections 1 and 2 above on 136 | a medium customarily used for software interchange; or, 137 | 138 | * **b)** Accompany it with a written offer, valid for at least three years, to 139 | give any third party, for a charge no more than your cost of physically 140 | performing source distribution, a complete machine-readable copy of the 141 | corresponding source code, to be distributed under the terms of Sections 1 142 | and 2 above on a medium customarily used for software interchange; or, 143 | 144 | * **c)** Accompany it with the information you received as to the offer to 145 | distribute corresponding source code. (This alternative is allowed only for 146 | noncommercial distribution and only if you received the program in object 147 | code or executable form with such an offer, in accord with Subsection b 148 | above.) 149 | 150 | The source code for a work means the preferred form of the work for making 151 | modifications to it. For an executable work, complete source code means all the 152 | source code for all modules it contains, plus any associated interface 153 | definition files, plus the scripts used to control compilation and installation 154 | of the executable. However, as a special exception, the source code distributed 155 | need not include anything that is normally distributed (in either source or 156 | binary form) with the major components (compiler, kernel, and so on) of the 157 | operating system on which the executable runs, unless that component itself 158 | accompanies the executable. 159 | 160 | If distribution of executable or object code is made by offering access to copy 161 | from a designated place, then offering equivalent access to copy the source code 162 | from the same place counts as distribution of the source code, even though third 163 | parties are not compelled to copy the source along with the object code. 164 | 165 | **4.** You may not copy, modify, sublicense, or distribute the Program except as 166 | expressly provided under this License. Any attempt otherwise to copy, modify, 167 | sublicense or distribute the Program is void, and will automatically terminate 168 | your rights under this License. However, parties who have received copies, or 169 | rights, from you under this License will not have their licenses terminated so 170 | long as such parties remain in full compliance. 171 | 172 | **5.** You are not required to accept this License, since you have not signed 173 | it. However, nothing else grants you permission to modify or distribute the 174 | Program or its derivative works. These actions are prohibited by law if you do 175 | not accept this License. Therefore, by modifying or distributing the Program (or 176 | any work based on the Program), you indicate your acceptance of this License to 177 | do so, and all its terms and conditions for copying, distributing or modifying 178 | the Program or works based on it. 179 | 180 | **6.** Each time you redistribute the Program (or any work based on the 181 | Program), the recipient automatically receives a license from the original 182 | licensor to copy, distribute or modify the Program subject to these terms and 183 | conditions. You may not impose any further restrictions on the recipients' 184 | exercise of the rights granted herein. You are not responsible for enforcing 185 | compliance by third parties to this License. 186 | 187 | **7.** If, as a consequence of a court judgment or allegation of patent 188 | infringement or for any other reason (not limited to patent issues), conditions 189 | are imposed on you (whether by court order, agreement or otherwise) that 190 | contradict the conditions of this License, they do not excuse you from the 191 | conditions of this License. If you cannot distribute so as to satisfy 192 | simultaneously your obligations under this License and any other pertinent 193 | obligations, then as a consequence you may not distribute the Program at all. 194 | For example, if a patent license would not permit royalty-free redistribution of 195 | the Program by all those who receive copies directly or indirectly through you, 196 | then the only way you could satisfy both it and this License would be to refrain 197 | entirely from distribution of the Program. 198 | 199 | If any portion of this section is held invalid or unenforceable under any 200 | particular circumstance, the balance of the section is intended to apply and the 201 | section as a whole is intended to apply in other circumstances. 202 | 203 | It is not the purpose of this section to induce you to infringe any patents or 204 | other property right claims or to contest validity of any such claims; this 205 | section has the sole purpose of protecting the integrity of the free software 206 | distribution system, which is implemented by public license practices. Many 207 | people have made generous contributions to the wide range of software 208 | distributed through that system in reliance on consistent application of that 209 | system; it is up to the author/donor to decide if he or she is willing to 210 | distribute software through any other system and a licensee cannot impose that 211 | choice. 212 | 213 | This section is intended to make thoroughly clear what is believed to be a 214 | consequence of the rest of this License. 215 | 216 | **8.** If the distribution and/or use of the Program is restricted in certain 217 | countries either by patents or by copyrighted interfaces, the original copyright 218 | holder who places the Program under this License may add an explicit 219 | geographical distribution limitation excluding those countries, so that 220 | distribution is permitted only in or among countries not thus excluded. In such 221 | case, this License incorporates the limitation as if written in the body of this 222 | License. 223 | 224 | **9.** The Free Software Foundation may publish revised and/or new versions of 225 | the General Public License from time to time. Such new versions will be similar 226 | in spirit to the present version, but may differ in detail to address new 227 | problems or concerns. 228 | 229 | Each version is given a distinguishing version number. If the Program specifies 230 | a version number of this License which applies to it and "any later version", 231 | you have the option of following the terms and conditions either of that version 232 | or of any later version published by the Free Software Foundation. If the 233 | Program does not specify a version number of this License, you may choose any 234 | version ever published by the Free Software Foundation. 235 | 236 | **10.** If you wish to incorporate parts of the Program into other free programs 237 | whose distribution conditions are different, write to the author to ask for 238 | permission. For software which is copyrighted by the Free Software Foundation, 239 | write to the Free Software Foundation; we sometimes make exceptions for this. 240 | Our decision will be guided by the two goals of preserving the free status of 241 | all derivatives of our free software and of promoting the sharing and reuse of 242 | software generally. 243 | 244 | 245 | No Warranty 246 | ----------- 247 | 248 | **11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR 249 | THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE 250 | STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM 251 | "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, 252 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 253 | PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 254 | PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 255 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 256 | 257 | **12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 258 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 259 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 260 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR 261 | INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA 262 | BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 263 | FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER 264 | OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. --------------------------------------------------------------------------------