Created
July 5, 2010 02:56
-
-
Save manveru/463953 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env coffee | |
# Ext JS Connect | |
# Copyright(c) 2010 Ext JS, Inc. | |
# MIT Licensed | |
# Module dependencies. | |
child_process: require 'child_process' | |
netBinding: process.binding 'net' | |
dirname: require('path').dirname | |
http: require 'http' | |
sys: require 'sys' | |
fs: require 'fs' | |
cwd: process.cwd() | |
net: require 'net' | |
# Framework version. | |
version: '0.2.0' | |
# Use child process workers to utilize all cores | |
workers: null | |
# Verbose output. | |
verbose: true | |
# Colored terminal output. | |
useColors: true | |
# Environment defaults. | |
env: { | |
name: process.env.CONNECT_ENV || 'development' | |
logfile: 'logs/connect.log' | |
pidfile: 'pids/connect.pid' | |
port: 3000 | |
host: null | |
} | |
process.connectEnv: env | |
# Usage documentation. | |
usage: """ | |
[bold]{Usage}: connect [options] start|stop|restart|status | |
[bold]{Options}: | |
-H, --host ADDR Host address, defaults to INADDR_ANY | |
-p, --port NUM Port number, defaults to 3000 | |
-n, --workers NUM Number of worker processes to spawn | |
-I, --include PATH Unshift the given path to require.paths | |
-E, --env NAME Set environment, defaults to "development" | |
-e, --eval CODE Evaluate the given string | |
-C, --chdir PATH Change to the given path | |
-c, --config PATH Load configuration module | |
-P, --pidfile PATH PID file, defaults to pids/connect.pid | |
-l, --logfile PATH Log file, defaults to logs/connect.log | |
-u, --user ID|NAME Change user with setuid() | |
-g, --group ID|NAME Change group with setgid() | |
-v, --verbose Enable verbose output | |
-V, --version Output connect version | |
-K, --no-color Suppress colored terminal output | |
-h, --help Outputy help information | |
--ENV VAL Sets the given connect environment variable | |
[bold]{Documentation}: | |
man connect | |
man connect-MIDDLEWARE | |
""" | |
# Log the given msg to stderr. | |
# @param {String} msg | |
log: (msg) -> | |
sys.error "... ${colorize msg}" if verbose | |
# Colorize the given string when color is enabled, | |
# otherwise strip placeholders. | |
# | |
# @param {String} str | |
# @return {String} | |
colorize: (str) -> | |
if useColors | |
colors: {bold: 1} | |
else | |
colors: {} | |
str.replace /\[(\w+)\]\{([^}]+)\}/g, (_, color, str) -> | |
"\x1B[${colors[color]}m${str}\x1B[0m" | |
# Ad-hoc sync mkdir -p implementation. | |
# | |
# @param {String} path | |
# @param {Number} mode | |
mkdirs: (path, mode) -> | |
segs: path.split "/" | |
mode ?= 0755 | |
if segs[0] | |
segs.unshift process.cwd() | |
else | |
segs[0] = '/'; | |
for seg, index in segs | |
dir: segs[0..index].join('/') | |
dir: dir.slice(1) if dir[1] is '/' | |
try | |
stat: fs.statSync dir | |
unless stat.isDirectory() | |
throw new Error("Failed to mkdir '${dir}'") | |
catch err | |
throw err unless err.errno is process.ENOENT | |
fs.mkdirSync dir, mode | |
# Strip ".js" or ".coffee" extension from the given path. | |
# | |
# @param {String} path | |
# @return {String} | |
modulePath: (path) -> | |
path.replace(/\.(js|coffee)$/, '') | |
# Exit with the given message. | |
# | |
# @param {String} msg | |
# @param {Number} code | |
abort: (msg, code) -> | |
sys.error colorize(msg) | |
process.exit code || 1 | |
# Load the given configuration file. | |
# | |
# @param {String} file | |
loadConfig: (file) -> | |
file: "${process.cwd()}/${file}" | |
log "loading config [bold]{`${file}'}" | |
args: [] | |
config: require file | |
for [key, values] in config | |
unless values instanceof Array | |
values: [values] | |
# Prefix flag | |
key: "--${key}" | |
# Apply flags | |
for value in values | |
if value is true | |
log " ${key} " | |
else | |
log " ${key} ${sys.inspect(value)}" | |
args.push key | |
args.push value unless typeof value is 'boolean' | |
parseArguments args | |
# Return pid from the given path. | |
# | |
# @param {String} path | |
# @return {Number} | |
getpid: (path) -> | |
try | |
parseInt(fs.readFileSync(path,), 10) | |
catch err | |
throw err if err.errno isnt process.ENOENT | |
log "[bold]{${err.path}} doesn't exist" | |
# Check status of the given pid. | |
# | |
# @param {Number} pid | |
checkStatus: (pid) -> | |
if pid | |
try | |
verbose: true | |
process.kill pid, 0 | |
log "[bold]{${pid}} is running" | |
catch err | |
throw err unless err.message is 'No such process' | |
log "[bold]{${pid}} is not running" | |
# Check status of process(es). | |
status: -> | |
checkStatus(getpid(env.pidfile)) | |
for i in [0..workers] | |
checkStatus(getpid(workerPidfile(i))) | |
# Attempt to stop the given pid. | |
# | |
# @param {Number} pid | |
stopProcess: (pid) -> | |
if pid | |
try | |
process.kill pid, 'SIGTERM' | |
log "Killed [bold]{${pid}}" | |
catch err | |
throw err unless err.message is 'No such process' | |
log "process [bold]{${pid} is not running" | |
# Stop process(es). | |
stop: (pid) -> | |
stopProcess(getpid(env.pidfile)) | |
for i in [0..workers] | |
stopProcess(getpid(workerPidfile(i))) | |
# Check if the given path exists (sync). | |
# | |
# @param {String} path | |
# @return {Boolean} | |
exists: (path) -> | |
try | |
fs.statSync(path) | |
catch err | |
false | |
# Return worker pidfile for the given index. | |
# | |
# @param {Number} i | |
# @return {String} | |
workerPidfile: (i) -> | |
env.pidfile.replace '.pid', ".${i}.pid" | |
# Require application module at the given path, | |
# which must be an instanceof net.Server, otherwise | |
# abort. | |
# | |
# @param {String} path | |
# @return {Server} | |
requireApp: (path) -> | |
app: require path | |
try | |
throw new Error('invalid server') unless app instanceof net.Server | |
app | |
catch err | |
abort """invalid application. | |
at: `${path}' | |
must export a , ex: `module.exports = http.createServer(...);' | |
""" | |
# Get path to application. | |
# | |
# - supports env.appPath | |
# - auto-detects {app,server}.{js,coffee} | |
# | |
# @return {String} | |
getAppPath: -> | |
path: "${process.cwd()}/${env.appPath || ''}" | |
# App path not given, try app.js and server.js | |
unless env.appPath | |
for name in ['app', 'server'] | |
for ext in ['.coffee', '.js'] | |
temp: "${path}${name}${ext}" | |
if exists temp | |
return modulePath(temp) | |
abort 'app not found, pass a module path, or create {app,server}.{coffee,js}' | |
path | |
# Start child worker process. | |
startWorker: -> | |
stdin new net.Stream(0, 'unix') | |
stdin.addListener 'data', (json) -> | |
env: JSON.parse json.toString() | |
stdin.addListener 'fd', (fd) -> | |
app: requireApp getAppPath() | |
app.env: env | |
[pid, host, port, name]: [process.pid, (env.host || '*'), env.port, env.name] | |
sys.error "Connect server(${pid}) listening on http://${host}:${port} in ${name} mode" | |
app.listenFD fd, 'tcp4' | |
stdin.resume() | |
startWorkers: -> | |
# Ensure logfile and pidfile directories are available | |
mkdirs dirname(env.logfile) | |
mkdirs dirname(env.pidfile) | |
errlogfile: env.logfile.replace('.log', '.error.log') | |
logStream: fs.createWriteStream env.logfile, {flags: 'a'} | |
errlogStream: fs.createWriteStream errlogfile, {flags: 'a'} | |
fd: netBinding.socket 'tcp4' | |
netBinding.bind fd, env.port | |
netBinding.listen fd, 128 | |
for i in [0..workers] | |
# Create an unnamed unix socket to pass the fd to the child. | |
fds: netBinding.socketpair() | |
# Spawn the child process | |
child: child_process.spawn( | |
process.argv[0], | |
[__filename, '--child'], | |
undefined, | |
[fds[1], -1, -1] | |
) | |
log "child spawned [bold]{${child.pid}}" | |
# For some reason stdin isn't getting set, patch it externally | |
child.stdin ||= new net.Stream(fds[0], 'unix') | |
# Write out worker pids | |
fs.writeFileSync workerPidfile(i), child.pid.toString(), 'ascii' | |
child.stdin.write JSON.stringify(env), 'ascii', fd | |
# Log stdout / stderr | |
child.stdout.addListener 'data', (data) -> | |
logStream.write data | |
child.stderr.addListener 'data', (data) -> | |
errlogStream.write data | |
# Start the process. | |
start: -> | |
log 'starting' | |
# Detect config.js | |
if exists './config.js' | |
log 'detected config.js' | |
loadConfig './config' | |
else if exists './config.coffee' | |
log 'detected config.coffee' | |
loadConfig './config' | |
# Application path | |
path: getAppPath() | |
# Spawn worker processes | |
if workers | |
if process.version < '0.1.98' | |
abort 'Cannot use workers with a version older than v0.1.98' | |
startWorkers() | |
return | |
# Load the app module | |
app: requireApp path | |
app.env: env | |
[pid, host, port, name]: [process.pid, (env.host || '*'), env.port, env.name] | |
sys.error "Connect server(${pid}) listening on http://${host}:${port} in ${name} mode" | |
app.listen env.port, env.host | |
# Parse the arguments. | |
parseArguments: (args, cmd) -> | |
arg: null | |
# Return shifted argument, or | |
# abort with the given prefix. | |
# | |
# @param {String} prefix | |
# @return {String} | |
requireArg: (prefix) -> | |
if args.length | |
args.shift() | |
else | |
abort "${prefix} requires an argument." | |
# Iterate | |
while args.length | |
switch arg: args.shift() | |
when '--child' | |
cmd: 'worker' | |
when '-h', '--help' | |
abort usage | |
when '-I', '--include' | |
require.paths.unshift requireArg('--include') | |
when '-e', '--eval' | |
eval requireArg('--eval') | |
when '-p', '--port' | |
env.port: parseInt(requireArg('--port'), 10) | |
when '-H', '--host' | |
env.host: requireArg('--host') | |
when '-u', '--user' | |
env.uid: requireArg('--user') | |
when '-g', '--group' | |
env.gid: requireArg('--group') | |
when '-C', '--chdir' | |
process.chdir requireArg('--chdir') | |
when '-l', '--logfile' | |
env.logfile: requireArg('--logfile') | |
when '-E', '--env' | |
env.name: requireArg('--env') | |
when '-P', '--pidfile' | |
env.pidfile: requireArg('--pidfile') | |
when '-c', '--config' | |
loadConfig(modulePath(requireArg('--config'))) | |
when '-v', '--verbose' | |
verbose: true | |
when '-V', '--version' | |
abort(version) | |
when '-K', '--no-color' | |
useColors: false | |
when '-n', '--workers' | |
workers: parseInt(requireArg('--workers'), 10) | |
when 'stop', 'start', 'restart', 'status' | |
cmd: arg | |
else | |
if arg[0] is '-' | |
arg: arg.substr(2) | |
env[arg]: requireArg("--${arg}") | |
else | |
env.appPath: modulePath(arg) | |
# Run the command | |
switch cmd | |
when 'stop' | |
stop() | |
when 'start' | |
start() | |
when 'restart' | |
stop() | |
start() | |
when 'status' | |
status() | |
when 'worker' | |
startWorker() | |
# Parse cli arguments | |
parseArguments process.argv.slice(2), 'start' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment