1 | 'use strict'
|
2 |
|
3 | const spawn = require('child_process').spawn
|
4 | const log = require('./log').getInstance()
|
5 | const path = require('path')
|
6 |
|
7 | class Script {
|
8 | constructor (name, code) {
|
9 | this.name = name
|
10 | this.code = code
|
11 | this.done = false
|
12 | this.pid = null
|
13 | this.child = null
|
14 | this.exitCode = 0
|
15 | this.onEnd = null
|
16 | this.duration = 0
|
17 | }
|
18 |
|
19 | setName (name) {
|
20 | this.name = name
|
21 | }
|
22 |
|
23 | isBackground () {
|
24 | return this.name.includes('+')
|
25 | }
|
26 |
|
27 | isPause () {
|
28 | return this.name === '-'
|
29 | }
|
30 |
|
31 | end () {
|
32 | this.done = true
|
33 | this.onEnd && this.onEnd()
|
34 | }
|
35 |
|
36 | hasEnded () {
|
37 | return this.done === true
|
38 | }
|
39 |
|
40 | hasFailed () {
|
41 | return !this.isValid() || this.exitCode !== 0
|
42 | }
|
43 |
|
44 | isRunning () {
|
45 | return this.pid !== null && !this.hasEnded()
|
46 | }
|
47 |
|
48 | isValid () {
|
49 | return this.code || this.isPause()
|
50 | }
|
51 |
|
52 | async start (onEnd) {
|
53 | this.onEnd = onEnd
|
54 |
|
55 |
|
56 | if (!this.isValid()) {
|
57 | log.err('runna', `Script ${this.name} is not valid.`)
|
58 | return
|
59 | }
|
60 |
|
61 | if (this.isPause()) {
|
62 | this.pid = 0
|
63 | return
|
64 | }
|
65 |
|
66 | if (this.isRunning()) {
|
67 | return
|
68 | }
|
69 |
|
70 | const spawnArgs = this.code.split(' ')
|
71 |
|
72 |
|
73 |
|
74 | return new Promise(resolve => {
|
75 | const timestamp = Date.now()
|
76 |
|
77 |
|
78 |
|
79 |
|
80 | log.dbg('runna', `Script ${this.name} started.`)
|
81 | const child = spawn(spawnArgs[0], spawnArgs.slice(1), { shell: true })
|
82 |
|
83 |
|
84 | let done
|
85 | const end = () => {
|
86 | if (!done) {
|
87 | this.duration = Date.now() - timestamp
|
88 | log.dbg('runna', `Script ${this.name} completed in ${this.duration} ms.`)
|
89 | this.end()
|
90 | done = resolve()
|
91 | }
|
92 | }
|
93 |
|
94 | child.on('close', code => {
|
95 | if (code !== 0) {
|
96 | log.err('runna', `Script ${this.name} exited with error code ${code}.`)
|
97 | this.fail(code)
|
98 | }
|
99 |
|
100 | end()
|
101 | })
|
102 |
|
103 | child.on('error', err => {
|
104 | log.err('runna', `Script ${this.name} threw an error.`)
|
105 | log.err(this.name, err)
|
106 | this.fail(1)
|
107 |
|
108 | end()
|
109 | })
|
110 |
|
111 |
|
112 | child.stderr.on('data', buf => {
|
113 | log.err(this.name, buf)
|
114 | if (!this.isBackground()) {
|
115 | this.fail(1)
|
116 | }
|
117 | })
|
118 |
|
119 |
|
120 | child.stdout.on('data', buf => {
|
121 | log.dbg(this.name, buf)
|
122 | })
|
123 |
|
124 |
|
125 | this.pid = child.pid
|
126 | this.child = child
|
127 | })
|
128 | }
|
129 |
|
130 | _getSpawnArgs (cmd, binaries) {
|
131 | const args = cmd.split(' ')
|
132 | const packageName = args[0]
|
133 |
|
134 |
|
135 | if (binaries[packageName]) {
|
136 | args[0] = binaries[packageName]
|
137 | }
|
138 |
|
139 | return [args, false]
|
140 | }
|
141 |
|
142 | fail (code) {
|
143 | this.exitCode = code
|
144 | }
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 | static getInstances (name, code, files = [], projects = []) {
|
151 | if (!name) {
|
152 | throw new Error('Script must have a name.')
|
153 | }
|
154 |
|
155 | const script = new Script(name, code)
|
156 | if (!script.isPause() && !code) {
|
157 | throw new Error(`Unknown script: ${name}`)
|
158 | }
|
159 |
|
160 |
|
161 | const scripts = []
|
162 | if (script.isPause() || script._isPlain()) {
|
163 | scripts.push(script)
|
164 | } else if (script._isProjOnly()) {
|
165 | for (const project of projects) {
|
166 | scripts.push(new Script(`${script.name}::${project}`, script.code.replace(/\$PROJ/g, project)))
|
167 | }
|
168 | } else if (script._isFileOnly()) {
|
169 | for (const file of files) {
|
170 | const suffix = file ? `::${path.basename(file)}` : ''
|
171 | scripts.push(new Script(`${script.name}${suffix}`, script.code.replace(/\$FILE/g, file)))
|
172 | }
|
173 | } else if (script._isProjAndFile()) {
|
174 | for (const project of projects) {
|
175 | const projCode = script.code.replace(/\$PROJ/g, project)
|
176 | for (const file of files) {
|
177 | const suffix = file ? `:${path.basename(file)}` : ''
|
178 | scripts.push(new Script(`${script.name}::${project}${suffix}`, projCode.replace(/\$FILE/g, file)))
|
179 | }
|
180 | }
|
181 | }
|
182 |
|
183 | return scripts
|
184 | }
|
185 |
|
186 | _isPlain (code) {
|
187 | return !this.code.includes('$PROJ') && !this.code.includes('$FILE')
|
188 | }
|
189 |
|
190 | _isProjOnly () {
|
191 | return this.code.includes('$PROJ') && !this.code.includes('$FILE')
|
192 | }
|
193 |
|
194 | _isFileOnly () {
|
195 | return !this.code.includes('$PROJ') && this.code.includes('$FILE')
|
196 | }
|
197 |
|
198 | _isProjAndFile () {
|
199 | return this.code.includes('$PROJ') && this.code.includes('$FILE')
|
200 | }
|
201 | }
|
202 |
|
203 | module.exports = Script
|