Skip to content

Instantly share code, notes, and snippets.

@manveru
Created July 12, 2010 03:15
Show Gist options
  • Save manveru/472091 to your computer and use it in GitHub Desktop.
Save manveru/472091 to your computer and use it in GitHub Desktop.
#!/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(
'coffee',
[__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, 'start'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment