/
server.coffee
180 lines (148 loc) · 5.19 KB
/
server.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# Muffin may run three different servers:
#
# 1. A web socket server for live reload
# 2. A dummy web server that serves client files from the build directory.
# This is useful when using Muffin as a static site generator, or when a server
# stack is not available.
# 3. A real Node.js or Google App Engine app server.
sysPath = require 'path'
url = require 'url'
http = require 'http'
net = require 'net'
async = require 'async'
{spawn} = require 'child_process'
_ = require 'underscore'
send = require 'send'
WebSocketServer = require('ws').Server
chokidar = require 'chokidar'
logging = require './utils/logging'
project = require './project'
# Files to ignore
ignored = (file) ->
filename = sysPath.basename(file)
/^\.|~$/.test(filename) or /\.swp/.test(filename) or file.match(project.buildDir) or file.match('node_modules')
# The `LiveReloadServer` sets up a web socket server to communicate with the browser.
class LiveReloadServer
constructor: (@port=9485) ->
@connections = []
start: ->
server = new WebSocketServer {port: @port}
server.on 'connection', (c) =>
@connections.push c
c.on 'close', =>
# Remove the connection on close.
i = @connections.indexOf(c)
@connections.splice(i, 1)
reloadBrowser: (path) ->
if path
switch sysPath.extname(path)
when '.css'
message = {reload: 'soft', type: 'css', path}
when '.png', '.jpg', '.jpeg', '.gif', '.ico'
message = {reload: 'soft', type: 'image', path}
else
message = {reload: 'hard'}
else
message = {reload: 'hard'}
@sendMessageToClients(message)
sendMessageToClients: (message) =>
connected = _(@connections).filter (c) -> c.readyState is 1
_(connected).each (c) -> c.send(JSON.stringify(message))
# The Node.js app server
class NodeAppServer
constructor: (options) ->
@port = options.port ? 4000
startAndWatch: ->
# Start the server
@start()
# Watch server files and restart the server when they change.
# Have to turn on 'polling' because FSWatcher has a lot of issues.
# Set polling interval to 200ms to reduce CPU usage.
watcher = chokidar.watch project.serverDir,
{ignored: ignored, persistent: true, ignoreInitial: true, usePolling: true, interval: 200}
watcher.on 'add', (path) =>
logging.info "added #{path}"
@start()
watcher.on 'change', (path) =>
logging.info "changed #{path}"
@start()
watcher.on 'unlink', (path) =>
logging.info "removed #{path}"
@start()
watcher.on 'error', (error) ->
logging.error "Error occurred while watching files: #{error}"
start: =>
child = exports.child
if child
child.shouldRestart = true
child.kill()
logging.info 'Restarting the application server...'
else
# Start the server in a child process
env = _.extend(process.env, {PORT: @port})
child = exports.child = spawn 'node', ['server/server.js'], {stdio: 'inherit', env}
child.shouldRestart = false
child.on 'exit', (code) =>
exports.child = null
if child.shouldRestart
# Restart the server when files change
process.nextTick(@start)
child.on 'uncaughtException', (err) ->
logging.error err.message
# Pass kill signals through to child
for signal in ['SIGTERM', 'SIGINT', 'SIGHUP', 'SIGQUIT']
process.on signal, ->
child.kill(signal)
process.exit 1
# The Google App Engine development server
class GAEAppServer
constructor: (options) ->
@port = options.port ? 4000
start: ->
console.log 'Starting Google App Engine development server...'
spawn 'dev_appserver.py', ["--port=#{@port}", project.serverDir], {stdio: 'inherit'}
# Public interface
# ----------------
liveReloadServer = null
startLiveReloadServer = ->
liveReloadServer = new LiveReloadServer(project.liveReloadPort)
liveReloadServer.start()
# Test if the live reload port is in use, and increment it as needed.
testLiveReloadPort = (callback) ->
port = 9485
portIsAvailable = false
test = -> portIsAvailable
fn = (done) ->
server = net.createServer()
server.on 'listening', ->
portIsAvailable = true
project.liveReloadPort = port
server.on 'close', done
server.close()
.on 'error', (err) ->
if err.code is 'EADDRINUSE'
port += 1
done(null)
.listen(port)
async.until test, fn, callback
reloadBrowser = (path) ->
liveReloadServer?.reloadBrowser(path)
startDummyWebServer = (options) ->
console.log 'Starting a dummy web server for the static files...'
app = http.createServer (req, res) ->
send(req, url.parse(req.url).pathname)
.root(project.buildDir)
.pipe(res)
port = options.port ? 4000
app.listen port, ->
console.log "Server is running at http://localhost:#{port}."
console.log "Quit the server with CONTROL-C."
startAppServer = (options) ->
switch project.config.serverType
when 'nodejs'
server = new NodeAppServer(options)
server.startAndWatch()
when 'gae'
server = new GAEAppServer(options)
server.start()
module.exports = {startLiveReloadServer, testLiveReloadPort, reloadBrowser, startDummyWebServer, startAppServer}