UNPKG

4.79 kBJavaScriptView Raw
1'use strict'
2
3const {Command} = require('heroku-cli-command')
4const path = require('path')
5const dirs = require('../lib/dirs')
6const lock = require('rwlockfile')
7const config = require('../lib/config')
8const errors = require('../lib/errors')
9const fs = require('fs-extra')
10
11class Update extends Command {
12 async run () {
13 if (config.disableUpdate) this.warn(config.disableUpdate)
14 else {
15 this.action(`${config.name}: Updating CLI`)
16 let channel = this.args.channel || config.channel
17 this.manifest = await this.fetchManifest(channel)
18 if (config.version === this.manifest.version && channel === config.channel) {
19 this.action.done(`already on latest version: ${config.version}`)
20 } else {
21 this.action(`${config.name}: Updating CLI to ${this.color.green(this.manifest.version)}${channel === 'stable' ? '' : ' (' + this.color.yellow(channel) + ')'}`)
22 await this.update(channel)
23 this.action.done()
24 }
25 }
26 this.action(`${config.name}: Updating plugins`)
27 }
28
29 async fetchManifest (channel) {
30 try {
31 let url = `https://${config.s3.host}/${config.name}/channels/${channel}/${process.platform}-${process.arch}`
32 return await this.http.get(url)
33 } catch (err) {
34 if (err.statusCode === 403) throw new Error(`HTTP 403: Invalid channel ${channel}`)
35 throw err
36 }
37 }
38
39 async update (channel) {
40 let url = `https://${config.s3.host}/${config.name}/channels/${channel}/${this.base}.tar.gz`
41 let stream = await this.http.get(url, {raw: true})
42 let dir = path.join(dirs.data, 'cli')
43 let tmp = path.join(dirs.data, 'cli_tmp')
44 await this.extract(stream, tmp)
45 await lock.write(dirs.updatelockfile, {skipOwnPid: true})
46 fs.removeSync(dir)
47 fs.renameSync(path.join(tmp, this.base), dir)
48 fs.removeSync(tmp)
49 }
50
51 extract (stream, dir) {
52 const zlib = require('zlib')
53 const tar = require('tar-stream')
54
55 return new Promise(resolve => {
56 fs.removeSync(dir)
57 let extract = tar.extract()
58 extract.on('entry', (header, stream, next) => {
59 let p = path.join(dir, header.name)
60 let opts = {mode: header.mode}
61 switch (header.type) {
62 case 'directory':
63 fs.mkdirpSync(p, opts)
64 next()
65 break
66 case 'file':
67 stream.pipe(fs.createWriteStream(p, opts))
68 break
69 case 'symlink':
70 // ignore symlinks since they will not work on windows
71 next()
72 break
73 default: throw new Error(header.type)
74 }
75 stream.resume()
76 stream.on('end', next)
77 })
78 extract.on('finish', resolve)
79 stream
80 .pipe(zlib.createGunzip())
81 .pipe(extract)
82 })
83 }
84
85 get base () {
86 return `${config.name}-v${this.manifest.version}-${process.platform}-${process.arch}`
87 }
88
89 async restartCLI () {
90 await lock.read(dirs.updatelockfile)
91 lock.unreadSync(dirs.updatelockfile)
92 const {spawnSync} = require('child_process')
93 const {status} = spawnSync(config.reexecBin, process.argv.slice(2), {stdio: 'inherit', shell: true})
94 process.exit(status)
95 }
96
97 get autoupdateNeeded () {
98 try {
99 const fs = require('fs-extra')
100 const moment = require('moment')
101 const stat = fs.statSync(dirs.autoupdatefile)
102 return moment(stat.mtime).isBefore(moment().subtract(4, 'hours'))
103 } catch (err) {
104 if (err.code !== 'ENOENT') console.error(err.stack)
105 return true
106 }
107 }
108
109 async autoupdate () {
110 try {
111 if (!this.autoupdateNeeded) return
112 fs.writeFileSync(dirs.autoupdatefile, '')
113 if (config.disableUpdate) await this.warnIfUpdateAvailable()
114 await this.checkIfUpdating()
115 let fd = fs.openSync(dirs.autoupdatelog, 'a')
116 const {spawn} = require('child_process')
117 spawn(dirs.reexecBin, ['update'], {stdio: [null, fd, fd], detached: true})
118 .on('error', errors.logError)
119 } catch (err) {
120 this.error('error autoupdating')
121 this.error(err)
122 errors.logError(err)
123 }
124 }
125
126 async warnIfUpdateAvailable () {
127 const manifest = await this.fetchManifest(config.channel)
128 let local = config.version.split('.')
129 let remote = manifest.version.split('.')
130 if (local[0] !== remote[0] || local[1] !== remote[1]) {
131 console.error(`${config.name}: update available from ${config.version} to ${manifest.version}`)
132 }
133 }
134
135 async checkIfUpdating () {
136 const lock = require('rwlockfile')
137 if (await lock.hasWriter(dirs.updatelockfile)) {
138 console.error(`${config.name}: warning: update in process`)
139 await this.restartCLI()
140 } else await lock.read(dirs.updatelockfile)
141 }
142}
143
144Update.topic = 'update'
145Update.args = [
146 {name: 'channel', optional: true}
147]
148
149module.exports = Update