/
appentity.js
257 lines (231 loc) · 6.67 KB
/
appentity.js
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// appentity.js
// Manage data for one app
// © Harald Rudell 2012 MIT License
var require = require('apprunner').getRequire(require)
var pidlink = require('./pidlink')
var watchit = require('./watchit')
var watchcopy = require('./watchcopy')
var appstate = require('./appstate')
// https://github.com/haraldrudell/haraldutil
var haraldutil = require('haraldutil')
// http://nodejs.org/api/path.html
var path = require('path')
module.exports = {
AppEntity: AppEntity,
eventListener: eventListener,
}
var listener
// min seconds between crashes
minSecondsCrashToCrash = 3
// constructor
// conf object that the created object takes over
// .name: string application name eg. 'Node.js #3'
// .id: string machine-friendly name eg. 'nodejs3'
// .folder: string fully qualified path to this app's folder
// .start: string or array of string: command to launch this app eg '.../app.js'
// .watchFiles
// .watchCopy: key: to file or folder, value: from file or folder, may be relative to conf.folder
// .state
function AppEntity(conf, parentData) {
var hadParentData = !!parentData
if (!parentData) parentData = {}
var crashCount = parentData.crashCount || 0
var exitCode = parentData.exitCode
var appState = appstate.appState(conf.id, childCallback, conf.start, conf.debug, parentData.state)
var lastLaunch = parentData.lastLaunch
var lastCrash = parentData.lastCrash
var pid = parentData.pid
var webInfo = {
PORT: 0,
URL: '',
}
var lastState = {}
// init file watchers
var myWatch = new watchit.WatchIt(doWatcherRestart, watchNotify)
myWatch.updateFiles(conf.watchFiles || [], conf.folder)
if (parentData.state == 'run' || parentData.state == 'debug') myWatch.activate()
// init filesystem copy watcher
var watchCopy = watchcopy.watchCopy(conf.id)
watchCopy.init(conf.watchCopy || {}, conf.folder, doWatcherRestart, function (err) {
if (err) throw err
if (!parentData.state) appState.doCommand(conf.state)
if (hadParentData && // this process had a status in master nodegod
(parentData.state == 'run' || parentData.state == 'debug') && // and it is probably up
conf.signal) { // and configured to receive signals
// send a signal that requests url and port for the running app
pidlink.getData(pid, conf.id, webInfoCb)
}
})
return {
update: update,
doCommand: appState.doCommand,
getState: getState,
}
// update this app's configuration
function update(o) {
var doWatch
var restart
var count = 0
conf.signal = o.signal
conf.name = o.name
if (stringOrArrayDifferent(conf.start, o.start)) {
conf.start = o.start
restart = 'start command change'
}
if (o.folder != conf.folder) {
conf.folder = o.folder
restart = 'folder change'
}
myWatch.updateFiles(conf.watchFiles = o.watchFiles, conf.folder)
watchCopy.updateCopyObject(conf.watchCopy = o.watchCopy || {}, conf.folder, cb)
cb(null, restart)
function cb(err, restartReason) {
if (err) throw err
if (restartReason) restart = restartReason
if (++count == 2) {
if (restart) console.log('Restarting ' + conf.name + ' on update due to ' + restart)
appState.doCommand(restart ? 'restart' : conf.state)
}
}
}
function watchitTrigger() {
myWatch.updateFiles(conf.watchFiles, conf.folder)
doWatcherRestart()
}
function watchCopyTrigger() {
watchCopy.updateCopyObject(conf.watchCopy, conf.folder, function (err, restart) {
if (err) throw err // TODO fix
})
doWatcherRestart()
}
// restart due to file change or copy change
function doWatcherRestart() {
if (appState.getState() != 'stop') {
console.log('Restarting ' + conf.name + ' due to file watch trigger')
appState.doCommand('restart')
} else console.log('Watcher restart ignored in stop state')
}
function webInfoCb(err, data) {
if (!err) {
if (data && !isSame(webInfo, data)) {
webInfo = data
sendUpdate()
}
} else console.error(err) // TODO
}
function isSame(o1, o2) {
var t1 = typeof o1
var same = t1 == typeof o2
if (same && t1 == 'object') {
var keys = Object.keys(o1)
var same = keys.length == Object.keys(o2).length
if (same) {
same = keys.every(function (prop) {
return o1[prop] == o2[prop]
})
}
}
return same
}
function childCallback(event, value, val2) {
switch (event) {
case 'exit':
exitCode = value
value = 0
case 'pid':
pid = value
if (pid && !val2 && conf.signal) pidlink.getData(pid, conf.id, webInfoCb)
break
case 'state':
switch (value) {
case 'stop': // child process was intentionally stopped
myWatch.deactivate() // no restarts on file changes
pid = 0
case 'stopping':
break
case 'run':
case 'debug':
lastLaunch = Date.now()
myWatch.activate() // activate watching of files
break
case 'crash':
// crash notify
var previousCrash = lastCrash
lastCrash = Date.now()
crashCount++
pid = 0
// crash recovery
var doRecovery = !previousCrash
if (!doRecovery) {
var elapsed = Math.floor((lastCrash - previousCrash) / 1000)
doRecovery = elapsed >= minSecondsCrashToCrash
}
if (doRecovery) appState.recover()
break
default:
throw Error('Bad state:' + value)
}
sendUpdate()
break
case 'childLog':
console.log(conf.id, value)
break
default:
throw Error('Bad event:' + event)
}
}
function watchNotify(event, filename) {
console.log(conf.id, event, filename)
}
function getState() {
return {
state: appState.getState(),
name: conf.name,
id: conf.id,
crashCount: crashCount,
lastLaunch: lastLaunch,
lastCrash: lastCrash,
exitCode: exitCode,
pid: pid,
watchers: myWatch.getCount(),
port: webInfo && webInfo.PORT || 0,
url: webInfo && webInfo.URL || '',
}
}
function sendUpdate() {
var state = logDifference(getState())
if (listener) listener(state)
}
function logDifference(state) {
var s = ['Update', state.name + ':']
for (var p in state) if (state[p] != lastState[p]) {
var v = state[p]
if (p == 'lastCrash' || p == 'lastLaunch') v = haraldutil.getISOPacific(new Date(v))
s.push(p + ':', v)
}
console.log(s.join(' '))
return lastState = state
}
}
// compare launch commands
// arguments are either string or array
function stringOrArrayDifferent(o, o1) {
var different = false
// check if same
// note: two arrays will not be same even if values are same
if (o != o1) {
if (Array.isArray(o) && Array.isArray(o1) &&
o.length == o1.length) {
// two arrays of the same length: examine elements
// loop until true is returned
different = o.some(function(element, index) {
return element != o1[index]
})
} else different = true
}
return different
}
// receive function for pushing model updates to clients
function eventListener(func) {
listener = func
}