Commit b08df4d

Anton Medvedev <anton@medv.io>
2022-08-28 14:05:26
Add halt() (#487)
* Add halt() * Add mode tsd tests
1 parent 088bbec
src/core.ts
@@ -104,7 +104,8 @@ export const $ = new Proxy<Shell & Options>(
       cmd += s + pieces[++i]
     }
     promise._bind(cmd, from, resolve!, reject!, getStore())
-    setImmediate(() => promise._run()) // Postpone run to allow promise configuration.
+    // Postpone run to allow promise configuration.
+    setImmediate(() => promise.isHalted || promise.run())
     return promise
   } as Shell & Options,
   {
@@ -143,6 +144,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
   private _timeout?: number
   private _timeoutSignal?: string
   private _resolved = false
+  private _halted = false
   private _piped = false
   _prerun = noop
   _postrun = noop
@@ -161,9 +163,9 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     this._snapshot = { ...options }
   }
 
-  _run() {
+  run(): ProcessPromise {
     const $ = this._snapshot
-    if (this.child) return // The _run() can be called from a few places.
+    if (this.child) return this // The _run() can be called from a few places.
     this._prerun() // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
     $.log({
       kind: 'cmd',
@@ -234,11 +236,12 @@ export class ProcessPromise extends Promise<ProcessOutput> {
       const t = setTimeout(() => this.kill(this._timeoutSignal), this._timeout)
       this.finally(() => clearTimeout(t)).catch(noop)
     }
+    return this
   }
 
   get stdin(): Writable {
     this.stdio('pipe')
-    this._run()
+    this.run()
     assert(this.child)
     if (this.child.stdin == null)
       throw new Error('The stdin of subprocess is null.')
@@ -246,7 +249,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
   }
 
   get stdout(): Readable {
-    this._run()
+    this.run()
     assert(this.child)
     if (this.child.stdout == null)
       throw new Error('The stdout of subprocess is null.')
@@ -254,21 +257,46 @@ export class ProcessPromise extends Promise<ProcessOutput> {
   }
 
   get stderr(): Readable {
-    this._run()
+    this.run()
     assert(this.child)
     if (this.child.stderr == null)
       throw new Error('The stderr of subprocess is null.')
     return this.child.stderr
   }
 
-  get exitCode() {
+  get exitCode(): Promise<number | null> {
     return this.then(
       (p) => p.exitCode,
       (p) => p.exitCode
     )
   }
 
-  pipe(dest: Writable | ProcessPromise) {
+  then<R = ProcessOutput, E = ProcessOutput>(
+    onfulfilled?:
+      | ((value: ProcessOutput) => PromiseLike<R> | R)
+      | undefined
+      | null,
+    onrejected?:
+      | ((reason: ProcessOutput) => PromiseLike<E> | E)
+      | undefined
+      | null
+  ): Promise<R | E> {
+    if (this.isHalted && !this.child) {
+      throw new Error('The process is halted!')
+    }
+    return super.then(onfulfilled, onrejected)
+  }
+
+  catch<T = ProcessOutput>(
+    onrejected?:
+      | ((reason: ProcessOutput) => PromiseLike<T> | T)
+      | undefined
+      | null
+  ): Promise<ProcessOutput | T> {
+    return super.catch(onrejected)
+  }
+
+  pipe(dest: Writable | ProcessPromise): ProcessPromise {
     if (typeof dest == 'string')
       throw new Error('The pipe() method does not take strings. Forgot $?')
     if (this._resolved) {
@@ -280,7 +308,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     this._piped = true
     if (dest instanceof ProcessPromise) {
       dest.stdio('pipe')
-      dest._prerun = this._run.bind(this)
+      dest._prerun = this.run.bind(this)
       dest._postrun = () => {
         if (!dest.child)
           throw new Error(
@@ -295,7 +323,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     }
   }
 
-  async kill(signal = 'SIGTERM') {
+  async kill(signal = 'SIGTERM'): Promise<void> {
     if (!this.child)
       throw new Error('Trying to kill a process without creating one.')
     if (!this.child.pid) throw new Error('The process pid is undefined.')
@@ -310,26 +338,35 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     } catch (e) {}
   }
 
-  stdio(stdin: IO, stdout: IO = 'pipe', stderr: IO = 'pipe') {
+  stdio(stdin: IO, stdout: IO = 'pipe', stderr: IO = 'pipe'): ProcessPromise {
     this._stdio = [stdin, stdout, stderr]
     return this
   }
 
-  nothrow() {
+  nothrow(): ProcessPromise {
     this._nothrow = true
     return this
   }
 
-  quiet() {
+  quiet(): ProcessPromise {
     this._quiet = true
     return this
   }
 
-  timeout(d: Duration, signal = 'SIGTERM') {
+  timeout(d: Duration, signal = 'SIGTERM'): ProcessPromise {
     this._timeout = parseDuration(d)
     this._timeoutSignal = signal
     return this
   }
+
+  halt(): ProcessPromise {
+    this._halted = true
+    return this
+  }
+
+  get isHalted(): boolean {
+    return this._halted
+  }
 }
 
 export class ProcessOutput extends Error {
test/core.test.js
@@ -433,4 +433,28 @@ test('$ is a regular function', async () => {
   assert.ok(typeof $.apply === 'function')
 })
 
+test('halt() works', async () => {
+  let filepath = `/tmp/${Math.random().toString()}`
+  let p = $`touch ${filepath}`.halt()
+  await sleep(1)
+  assert.not.ok(
+    fs.existsSync(filepath),
+    'The cmd called, but it should not have been called'
+  )
+  await p.run()
+  assert.ok(fs.existsSync(filepath), 'The cmd should have been called')
+})
+
+test('await on halted throws', async () => {
+  let p = $`sleep 1`.halt()
+  let ok = true
+  try {
+    await p
+    ok = false
+  } catch (err) {
+    assert.is(err.message, 'The process is halted!')
+  }
+  assert.ok(ok, 'Expected failure!')
+})
+
 test.run()
test-d/core.test-d.ts
@@ -30,7 +30,8 @@ expectType<ProcessPromise>(p.stdio('pipe'))
 expectType<ProcessPromise>(p.timeout('1s'))
 expectType<Promise<void>>(p.kill())
 expectType<Promise<ProcessOutput>>(p.then((p) => p))
-expectType<Promise<any>>(p.catch((p) => p))
+expectType<Promise<ProcessOutput>>(p.catch((p) => p))
+expectType<Promise<any>>(p.then((p) => p).catch((p) => p))
 
 let o = await p
 assert(o instanceof ProcessOutput)
test-d/experimental.test-d.ts
@@ -0,0 +1,19 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { expectType } from 'tsd'
+import { spinner } from '../src/experimental.js'
+
+expectType<string>(await spinner(() => 'foo'))
+expectType<string>(await spinner('title', () => 'bar'))