1 | 'use strict'
|
2 |
|
3 | const {Command} = require('heroku-cli-command')
|
4 | const path = require('path')
|
5 | const dirs = require('../lib/dirs')
|
6 | const lock = require('rwlockfile')
|
7 | const config = require('../lib/config')
|
8 | const errors = require('../lib/errors')
|
9 | const fs = require('fs-extra')
|
10 |
|
11 | class 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 |
|
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 |
|
144 | Update.topic = 'update'
|
145 | Update.args = [
|
146 | {name: 'channel', optional: true}
|
147 | ]
|
148 |
|
149 | module.exports = Update
|