Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,64 @@ const ee = appdmg({
});
```

The object returned from the `appdmg` function also has these methods and properties:

### ee.hasErrored

This property is initially `false`. It becomes `true` when appdmg encounters an error, and is cleaning up.

When `hasErrored` is `true`, avoid doing anything in an event handler that could throw. Doing so will prevent appdmg from cleaning up after an error (unmounting the temporary disk image, deleting it, and so on).

### ee.waitFor(promise)

Pauses execution until the given `Promise` completes. If the promise rejects, then the appdmg run is aborted. This lets you do custom asynchronous work on the disk image while it's being built.

For example, suppose your disk image will contain a folder called “Super Secret Folder”, which you want to be hidden from the Finder. Here's how to do it, using the Xcode command-line tools:

```javascript
const appdmg = require('appdmg');
const execa = require('execa');
const path = require('path');

const ee = appdmg({
// appdmg options go here
});

async function hideSecretFolder () {
// Use the SetFile program (it comes with Xcode) to hide `Super Secret Folder` from the Finder.
await execa('SetFile', [
'-a',
'V',
path.join(ee.temporaryMountPath, 'Super Secret Folder')
]);
}

ee.on('progress', info => {
if (!ee.hasErrored && info.type === 'step-begin' && info.title === 'Unmounting temporary image') {
ee.waitFor(hideSecretFolder());
// appdmg will now wait, until hideSecretFolder() is finished, before unmounting the temporary image.
}
})
```

### ee.abort(err)

Abort the appdmg run with `err` as the reason. It must be a truthy value, preferably an `Error`.

This method has no effect if appdmg has already encountered an error (indicated by `hasErrored` being `true`).

### ee.asPromise

A `Promise` that completes when appdmg is finished.

### ee.temporaryImagePath

Path to the temporary disk image. This is a writable disk image that appdmg creates and mounts while it's working.

### ee.temporaryMountPath

Path where the temporary disk image is currently mounted. This property is set when it's mounted, and deleted when it's unmounted.

## OS Support

Currently the only supported os is Mac OS X.
Expand Down
6 changes: 4 additions & 2 deletions lib/appdmg.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,11 @@ module.exports = exports = function (options) {
if (err) return next(err)

pipeline.addCleanupStep('unlink-temporary-image', 'Removing temporary image', function (next) {
delete pipeline.temporaryImagePath
fs.unlink(temporaryImagePath, next)
})

global.temporaryImagePath = temporaryImagePath
global.temporaryImagePath = pipeline.temporaryImagePath = temporaryImagePath
next(null)
})
})
Expand All @@ -226,10 +227,11 @@ module.exports = exports = function (options) {
if (err) return next(err)

pipeline.addCleanupStep('unmount-temporary-image', 'Unmounting temporary image', function (next) {
delete pipeline.temporaryMountPath
hdiutil.detach(temporaryMountPath, next)
})

global.temporaryMountPath = temporaryMountPath
global.temporaryMountPath = pipeline.temporaryMountPath = temporaryMountPath
next(null)
})
})
Expand Down
77 changes: 73 additions & 4 deletions lib/pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,42 @@ class Pipeline extends EventEmitter {
this.steps = []
this.totalSteps = 0
this.currentStep = 0
this._waitQueue = []
this.hasErrored = false

this.cleanupList = []
this.cleanupStore = {}
}

async _wait () {
// Drain the waitingFor queue. Wait on every promise that gets added to the queue. Only return once there are no promises left in the queue.
let waitOn, lastError

// Although assignment expressions are normally prohibited by StandardJS, the only other way I know of to write this is with a while (true) loop, which is dangerous.
// eslint-disable-next-line no-cond-assign
while (waitOn = this._waitQueue.pop()) {
try {
await waitOn
} catch (err) {
// Wait for all queued promises, not just until one of them rejects. Once the queue is empty, throw the last error (that isn't null or undefined), and log all other errors. That way, no one gets surprised by appdmg starting cleanup too soon.
if (lastError != null) {
console.error(lastError)
}

lastError = err
}
}

if (lastError != null) {
if (this.hasErrored) {
// Don't throw at all if appdmg is already cleaning up from an error.
console.error(lastError)
} else {
throw lastError
}
}
}

_progress (obj) {
obj.current = this.currentStep
obj.total = this.totalSteps
Expand All @@ -24,26 +55,26 @@ class Pipeline extends EventEmitter {
_runStep (step, nextAction, cb) {
const next = (err) => {
if (err) {
this._progress({ type: 'step-end', status: 'error' })
this.hasErrored = true
this._progress({ type: 'step-end', status: 'error' })
this.runRemainingCleanups(function (err2) {
if (err2) console.error(err2)
cb(err)
})
} else {
this._progress({ type: 'step-end', status: 'ok' })
this[nextAction](cb)
this._wait().then(() => this[nextAction](cb), cb)
}
}

next.skip = () => {
this._progress({ type: 'step-end', status: 'skip' })
this[nextAction](cb)
this._wait().then(() => this[nextAction](cb), cb)
}

this.currentStep++
this._progress({ type: 'step-begin', title: step.title })
step.fn(next)
this._wait().then(() => step.fn(next), next)
}

addStep (title, fn) {
Expand Down Expand Up @@ -98,15 +129,53 @@ class Pipeline extends EventEmitter {
process.nextTick(() => {
this._run((err) => {
if (err) {
this._completed = { err }
this.emit('error', err)
} else {
this._completed = true
this.emit('finish')
}
})
})

return this
}

waitFor (promise) {
this._waitQueue.push(promise)

// Suppress unhandled promise rejection warnings. Rejections will be handled later.
promise.catch(() => {})
}

abort (err) {
if (!this.hasErrored) {
this.waitFor(Promise.reject(err))
}
}

get asPromise () {
let { _asPromise: p } = this

if (!p) {
const { _completed: c } = this

if (c === true) {
p = Promise.resolve()
} else if (typeof c === 'object' && 'err' in c) {
p = Promise.reject(c.err)
} else {
p = new Promise((resolve, reject) => {
this.once('finish', resolve)
this.once('error', reject)
})
}

this._asPromise = p
}

return p
}
}

module.exports = Pipeline
Loading