This repository has been archived by the owner on Jul 9, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 14
/
application.coffee
653 lines (575 loc) · 23.9 KB
/
application.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
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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
fs = require "fs"
axon = require 'axon'
spawn = require('child_process').spawn
path = require('path')
log = require('printit')()
request = require("request-json-light")
colors = require "colors"
find = require 'lodash.find'
helpers = require './helpers'
homeClient = helpers.clients.home
proxyClient = helpers.clients.proxy
dsClient = helpers.clients.ds
client = helpers.clients.controller
makeError = helpers.makeError
getToken = helpers.getToken
# Applications helpers #
manifestBase = () ->
"domain": "localhost"
"repository":
"type": "git"
"scripts":
"start": "build/server.js"
# Define random function for application's token
randomString = (length) ->
string = ""
while (string.length < length)
string = string + Math.random().toString(36).substr(2)
return string.substr 0, length
waitInstallComplete = (slug, timeout, callback) ->
axon = require 'axon'
socket = axon.socket 'sub-emitter'
socket.connect 9105
noAppListErrMsg = """
No application listed after installation.
"""
appNotStartedErrMsg = """
Application is not running after installation.
"""
appNotListedErrMsg = """
Expected application not listed in database after installation.
"""
unless timeout?
timeout = 240000
if timeout isnt 'false'
timeoutId = setTimeout ->
socket.close()
homeClient.get "api/applications/", (err, res, apps) ->
if not apps?.rows?
callback new Error noAppListErrMsg
else
isApp = false
for app in apps.rows
if app.slug is slug and \
app.state is 'installed' and \
app.port
isApp = true
statusClient = request.newClient undefined
statusClient.host = "http://localhost:#{app.port}/"
statusClient.get "", (err, res) ->
if res?.statusCode in [200, 403]
callback null, state: 'installed'
else
callback new Error appNotStartedErrMsg
unless isApp
callback new Error appNotListedErrMsg
, timeout
socket.on 'application.update', (id) ->
dsClient.setBasicAuth 'home', token if token = getToken()
dsClient.get "data/#{id}/", (err, response, body) ->
if response.statusCode is 401
dsClient.setBasicAuth 'home', ''
dsClient.get "data/#{id}/", (err, response, body) ->
callback err, body
else if body.state is 'installed'
callback err, body
clearTimeout timeoutId
socket.close()
msgHomeNotStarted = (app) ->
return """
Install home failed for #{app}. The Cozy Home looks not started.
Install operation cannot be performed.
"""
msgRepoGit = (app, manifest) ->
return """
Install home failed for #{app}.
Default git repo #{manifest.git} doesn't exist.
You can use option -r to use a specific repo.
"""
msgLongInstall = (app) ->
return """
#{app} installation is still running. You should check for
its status later. If the installation is too long, you should try
to stop it by uninstalling the application and running the
installation again.
"""
msgInstallFailed = (app) ->
return """
Install home failed. Can't figure out the app state.
"""
# Applications functions #
# Callback all application stored in database
module.exports.getApps = (callback) ->
homeClient.get "api/applications/", (error, res, apps) ->
if apps? and apps.rows?
# sort apps by name
apps.rows.sort (a, b) -> if a.name < b.name then -1 else 1
callback null, apps.rows
else
# Check if couch is available
helpers.clients['couch'].get '', (err, res, body) ->
if err or not res? or res.statusCode isnt 200
log.error "CouchDB looks not started"
# Check if data-system is available
helpers.clients['ds'].get '', (err, res, body) ->
if not res? or res.statusCode isnt 200
log.error "The Cozy Data System looks not started"
# Check if home is available
helpers.clients['home'].get '', (err, res, body) ->
if not res? or res.statusCode isnt 200
log.error "The Cozy Home looks not started"
# Other pbs: credentials, view, ...
callback makeError(error, apps)
recoverManifest = (app, options, callback) ->
# Create manifest
manifest = manifestBase()
manifest.name = app
manifest.user = app
if options.displayName?
manifest.displayName = options.displayName
else
manifest.displayName = app
if options.repo
# If repository is specified callback it.
manifest.git = options.repo
manifest.branch = options.branch
# Check if repository have option branch after '@'.
repo = options.repo.split '@'
manifest.git = repo[0]
if repo.length is 2 and not options.branch?
manifest.branch = repo[1]
# Add .git if it is ommitted.
if manifest.git.slice(-4) isnt '.git'
manifest.git += '.git'
# Retrieve application icon.
homeClient.get 'api/applications/market', (err, res, market) ->
app = find market, (appli) -> appli.name is app
manifest.icon = app?.icon or ''
callback null, manifest
else
manifest.git = "https://github.com/cozy/cozy-#{app}.git"
# Check if application exists in market
homeClient.get 'api/applications/market', (err, res, market) ->
marketManifest = find market, (appli) -> appli.name is app
callback null, marketManifest or manifest
# Install application <app>
module.exports.install = install = (app, options, callback) ->
recoverManifest app, options, (err, manifest) ->
return callback err if err
what = "api/applications/install"
homeClient.headers['content-type'] = 'application/json'
homeClient.post what, manifest, (err, res, body) ->
if err or body.error
if err?.code is 'ECONNREFUSED'
err = makeError msgHomeNotStarted(app), null
else if body?.message?.toLowerCase().indexOf('not found') isnt -1
err = makeError msgRepoGit(app, manifest), null
else
err = makeError err, body
callback err
else
slug = body.app.slug
waitInstallComplete slug, options.timeout, (err, appresult) ->
if err
callback makeError(err, null)
else if appresult.state is 'installed'
callback()
else if appresult.state is 'installing'
callback makeError(msgLongInstall(app), null)
else
callback makeError(msgInstallFailed(app), null)
# Uninstall application <app>
uninstall = module.exports.uninstall = (app, callback) ->
what = "api/applications/#{app}/uninstall"
homeClient.del what, (err, res, body) ->
if err or body.error
callback makeError(err, body)
else
callback()
# Start application <app>
start = module.exports.start = (app, callback) ->
find = false
homeClient.get "api/applications/", (err, res, apps) ->
if apps? and apps.rows?
for manifest in apps.rows when manifest.name is app
find = true
what = "api/applications/#{manifest.slug}/start"
homeClient.post what, manifest, (err, res, body) ->
if err or body.error
callback makeError(err, body)
else
callback()
unless find
msg= "application #{app} not found."
callback makeError(msg)
else
msg = "no applications installed."
callback makeError(msg)
# Stop application <app>
stop = module.exports.stop = (app, callback) ->
find = false
homeClient.get "api/applications/", (err, res, apps) ->
if apps?.rows?
for manifest in apps.rows when manifest.name is app
find = true
what = "api/applications/#{app}/stop"
homeClient.post what, {}, (err, res, body) ->
if err? or body.error?
callback makeError(err, body)
else
callback()
unless find
err = "application #{app} not found"
callback makeError(err, null)
else
err = "application #{app} not found"
callback makeError(err, null)
# Update application <app>
module.exports.update = (app, callback) ->
find = false
homeClient.get "api/applications/", (err, res, apps) ->
if apps? and apps.rows?
for manifest in apps.rows
if manifest.name is app
find = true
what = "api/applications/#{manifest.slug}/update"
homeClient.put what, manifest, (err, res, body) ->
if err or body.error
callback makeError(err, body)
else
# Force authentication
process.env.NAME = "home"
process.env.TOKEN = helpers.getToken()
process.env.NODE_ENV = "production"
# remove update notification
NotificationsHelper = require 'cozy-notifications-helper'
notifier = new NotificationsHelper 'home'
notificationSlug = """
home_update_notification_app_#{app}
"""
notifier.destroy notificationSlug, (err) ->
log.error err if err?
callback()
if not find
err = "Update failed: application #{app} not found."
callback makeError(err, null)
else
err = "Update failed: no application installed"
callback makeError(err, null)
# Change stack application branch
module.exports.changeBranch = (app, branch, callback) ->
find = false
homeClient.get "api/applications/", (err, res, apps) ->
if apps? and apps.rows?
for manifest in apps.rows
if manifest.name is app
find = true
what = "api/applications/#{manifest.slug}/branch/#{branch}"
homeClient.put what, manifest, (err, res, body) ->
if err or body.error
callback makeError(err, body)
else
callback()
if not find
err = "Update failed: application #{app} not found."
callback makeError(err, null)
# Restart application <app>
module.exports.restart = (app, callback) ->
log.info "stop #{app}"
stop app, (err) ->
if err
callback err
else
log.info "start #{app}"
start app, callback
# Restop (start and stop) application <app>
module.exports.restop = (app, callback) ->
log.info "start #{app}"
start app, (err) ->
if err
callback err
else
log.info "stop #{app}"
stop app, callback
# Reinstall application <app>
module.exports.reinstall = (app, options, callback) ->
log.info " * uninstall #{app}"
uninstall app, (err) ->
if err
log.error ' -> KO'
callback err
else
log.info ' -> OK'
log.info " * install #{app}"
install app, options, (err)->
if err
log.error ' -> KO'
else
log.info ' -> OK'
callback err
# Intall application <app> from disk to database.
module.exports.installFromDisk = (app, callback) ->
options = {'headers': {'content-type': 'application/json'}}
helpers.retrieveManifestFromDisk app, (err, manifest) ->
# Create application document
appli =
docType: "application"
displayName: manifest.displayName or manifest.name.replace 'cozy-', ''
name: manifest.name.replace 'cozy-', ''
slug: manifest.name.replace 'cozy-', ''
version: manifest.version
isStoppable: false
package: manifest.package
git: manifest.git
branch: manifest.branch
state: 'installed'
iconPath: "img/apps/#{app}.svg"
iconType: 'svg'
port: null
clientCouch = helpers.clients.couch
[id, pwd] = helpers.getAuthCouchdb(false)
clientCouch.setBasicAuth id, pwd if id isnt ''
clientCouch.post helpers.dbName, appli, options, (err, res, app) ->
return callback err if err?
return callback app.error if app.error?
# Create access document
access =
docType: 'Access'
login: appli.name
token: randomString()
permissions: manifest['cozy-permissions']
app: app.id
clientCouch.post helpers.dbName, access, options, (err, res, body) ->
return callback err if err?
return callback app.error if app.error?
# Add icon
homePath = '/usr/local/cozy/apps/home/client/app/assets'
iconPath = path.join homePath, appli.iconPath
urlPath = "#{helpers.dbName}/#{app.id}/icon.svg?rev=#{app.rev}"
clientCouch.putFile urlPath, iconPath, (err, res, body) ->
callback()
# Install without home (usefull for relocation)
module.exports.installController = (app, callback) ->
log.info " * install #{app.slug}"
client.stop app.slug, (err, res, body) ->
# Retrieve application manifest
manifest = manifestBase()
manifest.name = app.slug
manifest.user = app.slug
manifest.repository.url = app.git
manifest.package = app.package
manifest.type = app.type
dsClient.setBasicAuth 'home', token if token = getToken()
dsClient.post 'request/access/byApp/', key: app.id, (err, res, body) ->
manifest.password = body[0].value.token
if app.branch?
manifest.repository.branch = app.branch
# Install (or start) application
client.start manifest, (err, res, body) ->
if err or body.error
log.error ' -> KO'
callback makeError(err, body)
else
log.info ' -> OK'
if body.drone.port isnt app.port and app.state is 'installed'
# Update port if it has changed
app.port = body.drone.port
log.info " * update port"
dsClient.put "data/#{app.id}/", app, (err, res, body) ->
if err or body?.error
log.error ' -> KO'
callback makeError(err, body)
else
log.info ' -> OK'
callback()
else
callback()
# Stop application without home (usefull for relocation)
module.exports.stopController = (app, callback) ->
log.info " * stop #{app}"
# Stop application
client.stop app, (err, res, body) ->
if err
log.error ' -> KO'
callback makeError(err, body)
else
log.info ' -> OK'
callback()
# Callback application version
module.exports.getVersion = (app, callback) ->
callback app.version
# Callback application state
module.exports.check = (options, app, url) ->
(callback) ->
colors.enabled = not options.raw? and not options.json?
statusClient = request.newClient url
statusClient.get "", (err, res) ->
if res?.statusCode in [200, 403]
if not options.json
log.raw "#{app}: " + "up".green
callback? null, [app, 'up']
else
if not options.json
log.raw "#{app}: " + "down".red
callback? null, [app, 'down']
## Usefull for application developpement
# Generate a random 32 char string.
randomString = (length=32) ->
string = ""
string += Math.random().toString(36).substr(2) while string.length < length
string.substr 0, length
removeApp = (apps, name, callback) ->
if apps.length > 0
app = apps.pop().value
if app.name is name
dsClient.del "access/#{app._id}/", (err, response, body) ->
dsClient.del "data/#{app._id}/", (err, response, body) ->
removeApp apps, name, callback
else
removeApp apps, name, callback
else
callback()
recoverStandaloneManifest = (port, cb) ->
unless fs.existsSync 'package.json'
log.error "Cannot read package.json. " +
"This function should be called in root application folder."
else
try
packagePath = path.relative __dirname, 'package.json'
manifest = require packagePath
catch err
log.raw err
log.error "Package.json isn't correctly formatted."
return
# Retrieve manifest from package.json
manifest.name = "#{manifest.name}test"
manifest.displayName =
manifest['cozy-displayName'] or manifest.name
manifest.state = "installed"
manifest.docType = "Application"
manifest.port = port
manifest.slug = manifest.name.replace 'cozy-', ''
access =
permissions: manifest['cozy-permissions']
password: randomString()
slug: manifest.slug
if manifest.slug in ['hometest', 'proxytest', 'data-systemtest']
log.error(
'Sorry, cannot start stack application without ' +
' controller.')
cb()
else
cb(manifest, access)
putInDatabase = (manifest, access, cb) ->
log.info "Add/replace application in database..."
token = getToken()
if token?
dsClient.setBasicAuth 'home', token
requestPath = "request/application/all/"
dsClient.post requestPath, {}, (err, response, apps) ->
log.error "Data-system looks down (not responding)." if err?
return cb() if err?
removeApp apps, manifest.name, () ->
dsClient.post "data/", manifest, (err, res, body) ->
id = body._id
if err
log.error "Cannot add application in database."
cb makeError(err, body)
else
access.app = id
dsClient.post "access/", access, (err, res, body) ->
if err
msg = "Cannot add application in database."
log.error msg
cb makeError(err, body)
else
cb()
## Start applicationn without controller in a production environment.
# * Add/Replace application in database (for home and proxy)
# * Reset proxy
# * Start application with environment variable
# * When application is stopped : remove application in database and reset proxy
module.exports.startStandalone = (port, callback) ->
appmanifest = null
process.on 'SIGINT', ->
stopStandalone (err) ->
if not err?
console.log "Application removed"
, appmanifest
process.on 'uncaughtException', (err) ->
log.error 'uncaughtException'
log.raw err
log.info "Retrieve application manifest..."
# Recover application manifest
recoverStandaloneManifest port, (manifest, access) ->
# Add/Replace application in database
appmanifest = manifest
putInDatabase appmanifest, access, (err) ->
return callback err if err?
# Reset proxy
log.info "Reset proxy..."
proxyClient.get "routes/reset", (err, res, body) ->
if err
log.error "Cannot reset routes."
return callback makeError(err, body)
else
# Add environment varaible.
log.info "Start application..."
process.env.TOKEN = access.password
process.env.NAME = access.slug
process.env.NODE_ENV = "production"
process.env.PORT = port
# Start application
server = spawn "npm", ["start"]
server.stdout.setEncoding 'utf8'
server.stdout.on 'data', (data) ->
log.raw data
server.stderr.setEncoding 'utf8'
server.stderr.on 'data', (data) ->
log.raw data
server.on 'error', (err) ->
log.raw err
server.on 'close', (code) ->
log.info "Process exited with code #{code}"
## Stop applicationn without controller in a production environment.
# * Remove application in database and reset proxy
# * Usefull if start-standalone doesn't remove app
stopStandalone = module.exports.stopStandalone = (callback, manifest=null) ->
log.info "Retrieve application manifest ..."
if not manifest
# Recover application manifest
unless fs.existsSync 'package.json'
error = "Cannot read package.json. " +
"This function should be called in root application folder"
return callback makeError(err, null)
try
packagePath = path.relative __dirname, 'package.json'
manifest = require packagePath
catch err
error "Package.json isn't in a correct format"
return callback makeError(err, null)
# Retrieve manifest from package.json
manifest.name = manifest.name + "test"
manifest.slug = manifest.name.replace 'cozy-', ''
if manifest.slug in ['hometest', 'proxytest', 'data-systemtest']
error = 'Sorry, cannot start stack application without controller.'
callback makeError(err, null)
else
# Add/Replace application in database
token = getToken()
unless token?
return
log.info "Remove from database ..."
dsClient.setBasicAuth 'home', token
requestPath = "request/application/all/"
dsClient.post requestPath, {}, (err, response, apps) ->
if err
return callback makeError("Data-system doesn't respond", null)
removeApp apps, manifest.name, () ->
log.info "Reset proxy ..."
proxyClient.get "routes/reset", (err) ->
if err
log.error "Cannot reset routes."
callback makeError(err, null)
else
callback()