'use strict'
const { fetchApi } = require('./util/fetch')
const EventEmitter = require('events')
const wsstream = require('websocket-stream')
const Snapshot = require('./snapshot')
const Agent = require('./agent')
const WebPlayer = require('./webplayer')
const Images = require('./images')
const pTimeout = require('p-timeout')
const NetworkMonitor = require('./netmon')
const Netdump = require('./netdump')
const { sleep } = require('./util/sleep')
const util = require('util')
const fs = require('fs')
const { compress, uploadFile } = require('./images')
const { v4: uuidv4 } = require('uuid')
const split = require('split')
/**
* @typedef {object} ThreadInfo
* @property {string} pid - process PID
* @property {string} kernelId - process ID in kernel
* @property {string} name - process name
* @property {object[]} threads - process threads
* @property {string} threads[].tid - thread ID
* @property {string} threads[].kernelId - thread ID in kernel
*/
/**
* @typedef {object} PanicInfo
* @property {integer} flags
* @property {string} panic
* @property {string} stackshot
* @property {string} other
* @property {integer} ts
*/
/**
* @typedef {object} PeripheralData
* @property {string} gpsToffs - GPS time offset
* @property {string} gpsLat - GPS latitude
* @property {string} gpsLon - GPS longitude
* @property {string} gpsAlt - GPS altitude
* @property {string} acOnline - Is AC charger present, options ['0' , '1']
* @property {string} batteryPresent - Is battery present, options ['0' , '1']
* @property {string} batteryStatus - Battery charging status, options ['charging', 'discharging', 'not-charging']
* @property {string} batteryHealth - Battery health, options ['good', 'overheat', 'dead', 'overvolt', 'failure']
* @property {string} batteryCapacity - Battery capacity, options 0-100
* @property {string} acceleration - Acceleration sensor
* @property {string} gyroscope - Gyroscope sensor
* @property {string} magnetic - Magnetic sensor
* @property {string} orientation - Orientation sensor
* @property {string} temperature - Temperator sensor
* @property {string} proximity - Proximity sensor
* @property {string} light - Light sensor
* @property {string} pressure - Pressure sensor
* @property {string} humidity - Humidity sensor
*/
/**
* @typedef {object} RateInfo
* Convert microcents to USD using rate / 1000000 / 100 * seconds where rate is rateInfo.onRateMicrocents or rateInfo.offRateMicrocents and seconds is the number of seconds the instance is running.
* @property {integer} onRateMicrocents - The amount per second, in microcents (USD), that this instance charges to be running.
* @property {integer} offRateMicrocents - The amount per second, in microcents (USD), that this instance charges to be stored.
*/
/**
* @typedef {number} RotationType
*
* Valid values are 1-4 where each number corresponds to a device orientation.
* 1. Portrait: The device is held upright, with the top edge at the top.
* 2. Portrait Vertically Inverted (Upside-Down): The device is held upright, but with the top edge at the bottom.
* 3. Landscape (Top of Device to the Left): The device is turned sideways with the top edge facing left.
* 4. Landscape (Top of Device to the Right): The device is turned sideways with the top edge facing right.
*/
/**
* Instances of this class are returned from {@link Project#instances}, {@link
* Project#getInstance}, and {@link Project#createInstance}. They should not be
* created using the constructor.
* @hideconstructor
*/
class Instance extends EventEmitter {
constructor (project, info) {
super()
this.project = project
this.info = info
this.infoDate = new Date()
this.id = info.id
this.updating = false
this.hash = null
this.hypervisorStream = null
this._agent = null
this._netmon = null
this._webplayer = null
this.lastPanicLength = null
this.volumeId = null
this.on('newListener', event => {
if (event === 'change') {
this.project.updater.add(this)
}
})
this.on('removeListener', event => {
if (event === 'change' && this.listenerCount('change') === 0) {
this.project.updater.remove(this)
}
})
}
/**
* The instance name.
*/
get name () {
return this.info.name
}
/**
* The instance state. Possible values include:
*
* State|Description
* -|-
* `on`|The instance is powered on.
* `off`|The instance is powered off.
* `creating`|The instance is in the process of creating.
* `deleting`|The instance is in the process of deleting.
* `deleted`|The instance is deleted, instance will set to undefined.
* `paused`|The instance is paused.
*
* A full list of possible values is available in the API documentation.
*/
get state () {
return this.info.state
}
/**
* The timestamp when the state last changed.
*/
get stateChanged () {
return this.info.stateChanged
}
/**
* The instance flavor, such as `iphone6`.
*/
get flavor () {
return this.info.flavor
}
/**
* The instance type, such as `ios`.
*/
get type () {
return this.info.type
}
/**
* The instance orientation
*/
get orientation () {
return this.info.orientation
}
/**
* The pending task that is being requested by the user and is being executed by the backend.
* This field is null when no tasks are pending. The returned object has two fields: name and options.
*
* Current options for name are start, stop, pause, unpause, snapshot, revert.
* For start and revert, options.bootOptions contains the boot options the instance is to be started with.
*
*/
get userTask () {
return this.info.userTask
}
/**
* Return the current task state.
*/
get taskState () {
return this.info.taskState
}
/**
* Rename an instance.
* @param {string} name - The new name of the instance.
* @example <caption>Renaming the first instance named `foo` to `bar`</caption>
* const instances = await project.instances();
* const instance = instances.find(instance => instance.name == 'foo');
* await instance.rename('bar');
*/
async rename (name) {
await this._fetch('', {
method: 'PATCH',
json: { name }
})
}
/**
* The instance boot options.
*/
get bootOptions () {
return this.info.bootOptions
}
/**
* Change boot options for an instance.
* @param {Object} bootOptions - The new boot options for the instance.
* @example <caption>Changing the boot arguments for the instance.</caption>
* const instances = await project.instances();
* const instance = instances.find(instance => instance.name == 'foo');
* await instance.modifyBootOptions(Object.assign({}, instance.bootOptions, {bootArgs: 'new-boot-args'}));
*/
async modifyBootOptions (bootOptions) {
await this._fetch('', {
method: 'PATCH',
json: { bootOptions }
})
}
/**
* Change device peripheral/sensor values.
*
* Currently available for Android only.
* @param {PeripheralData} peripheralData - The new peripheral options for the instance.
* @example <caption>Changing the sensor values for the instance.</caption>
* const instances = await project.instances();
* const instance = instances.find(instance => instance.name == 'foo');
* await instance.modifyPeripherals({
* "gpsToffs": "0.000000",
* "gpsLat": "37.414300",
* "gpsLon": "-122.077400",
* "gpsAlt": "45.000000",
* "acOnline": "1",
* "batteryPresent": "1",
* "batteryStatus": "discharging",
* "batteryHealth": "overheat",
* "batteryCapacity": "99.000000",
* "acceleration": "0.000000,9.810000,0.000000",
* "gyroscope": "0.000000,0.000000,0.000000",
* "magnetic": "0.000000,45.000000,0.000000",
* "orientation": "0.000000,0.000000,0.000000",
* "temperature": "25.000000",
* "proximity": "50.000000",
* "light": "20.000000",
* "pressure": "1013.250000",
* "humidity": "55.000000"
* }));
*/
async modifyPeripherals (peripheralData) {
await this._fetch('', {
method: 'PATCH',
json: { peripherals: peripheralData }
})
}
/**
* Change device orientation.
* @param {RotationType} rotation - The new rotation for the instance.
* @example await instance.rotate(1);
*/
async rotate (rotation) {
await this._fetch('/rotate', {
method: 'POST',
json: {
orientation: rotation
},
response: 'raw'
})
}
/**
* Get peripheral/sensor data
* @return {Promise<PeripheralData>}
* @example
* let peripherals = await instance.getPeripherals();
*/
async getPeripherals () {
return await this._fetch('/peripherals', { method: 'GET' })
}
/**
* Return an array of this instance's {@link Snapshot}s.
* @returns {Snapshot[]} This instance's snapshots
* @example
* const instances = await project.instances();
* const instance = instances.find(instance => instance.name == 'foo');
* await instance.snapshots();
*/
async snapshots () {
const snapshots = await this._fetch('/snapshots')
return snapshots.map(snap => new Snapshot(this, snap))
}
/**
* Take a new snapshot of this instance.
* @param {string} name - The name for the new snapshot.
* @returns {Snapshot} The new snapshot
* @example
* const instances = await project.instances();
* const instance = instances.find(instance => instance.name == 'foo');
* await instance.takeSnapshot("TestSnapshot");
*/
async takeSnapshot (name) {
const snapshot = await this._fetch('/snapshots', {
method: 'POST',
json: { name }
})
await this.update()
await this.waitForUserTask(null)
return new Snapshot(this, snapshot)
}
/**
* Returns a dump of this instance's serial port log.
* @return {string}
* @example
* const instances = await project.instances();
* const instance = instances.find(instance => instance.name == 'foo');
* console.log(await instance.consoleLog());
*/
async consoleLog () {
const response = await this._fetch('/consoleLog', { response: 'raw' })
return await response.text()
}
/**
* Return an array of recorded kernel panics.
* @return {Promise<PanicInfo[]>}
* @example
* const instances = await project.instances();
* const instance = instances.find(instance => instance.name == 'foo');
* console.log(await instance.panics());
*/
async panics () {
return await this._fetch('/panics')
}
/**
* Clear the recorded kernel panics of this instance.
* @example
* const instances = await project.instances();
* const instance = instances.find(instance => instance.name == 'foo');
* await instance.clearPanics();
*/
async clearPanics () {
await this._fetch('/panics', { method: 'DELETE' })
}
/**
* Return an {@link Agent} connected to this instance. Calling this
* method multiple times will reuse the same agent connection.
* @returns {Agent}
* @example
* let agent = await instance.agent();
* await agent.ready();
*/
async agent () {
if (this._agent && !this._agent.connected && !this._agent.pendingConnect) {
this._agent.disconnect()
delete this._agent
}
if (!this._agent) {
this._agent = await this.newAgent()
}
return this._agent
}
async agentEndpoint () {
// Extra while loop to avoid races where info.agent gets unset again before we wake back up.
while (!this.info.agent) await this._waitFor(() => !!this.info.agent)
// We want to avoid a situation where we were not listening for updates, and the info we have is stale (from last boot),
// and the instance has started again but this time with no agent info yet or new agent info. Therefore, we can use
// cached if only if it's recent.
if (new Date().getTime() - this.infoDate.getTime() > 2 * this.project.updater.updateInterval) {
try {
await this.update()
} catch (err) {
if (err.stack.includes('500 Internal Server Error')) {
return undefined
}
}
}
return this.project.api + '/agent/' + this.info.agent.info
}
async waitForAgentReady () {
let agentObtained
do {
try {
const endpoint = await this.agentEndpoint()
if (!endpoint) throw new Error('Instance likely does not exist')
const agent = await this.agent()
agentObtained = await pTimeout(
(async () => {
try {
await agent.ready()
return true
} catch (e) {
// If the websocket threw an error, lets wait for the end of
// the timeout to give it some breathing room
await sleep(2 * 1000)
} finally {
agent.disconnect()
}
})(),
20 * 1000,
async () => {
// When this times out, it is likely that the instance isn't fully up yet
await sleep(5 * 1000)
return false
}
)
} catch (e) {
console.log(`Caught error waiting for agent to be ready ${e}`)
if (e.stack.includes('Instance likely does not exist')) {
throw e
}
}
} while (!agentObtained)
}
/**
* Create a new {@link Agent} connection to this instance. This is
* useful for agent tasks that don't finish and thus consume the
* connection, such as {@link Agent#crashes}.
* @returns {Agent}
* @example
* let crashListener = await instance.newAgent();
* crashListener.crashes('com.corellium.demoapp', (err, crashReport) => {
* if (err) {
* console.error(err);
* return;
* }
* console.log(crashReport);
* });
*/
async newAgent () {
return new Agent(this)
}
/**
* Return {@link Netdump} connected to this instance. Calling this
* method multiple times will reuse the same agent connection.
* @returns {Netdump}
*/
async netdump () {
if (!this._netdump) this._netdump = await this.newNetdump()
return this._netdump
}
async netdumpEndpoint () {
// Extra while loop to avoid races where info.netdump gets unset again before we wake back up.
while (!this.info.netdump) await this._waitFor(() => !!this.info.netdump)
// We want to avoid a situation where we were not listening for updates, and the info we have is stale (from last boot),
// and the instance has started again but this time with no agent info yet or new agent info. Therefore, we can use
// cached if only if it's recent.
if (new Date().getTime() - this.infoDate.getTime() > 2 * this.project.updater.updateInterval) {
try {
await this.update()
} catch (err) {
if (err.stack.includes('500 Internal Server Error')) {
return undefined
}
}
}
return this.project.api + '/agent/' + this.info.netdump.info
}
/**
* Create a new {@link Netdump} connection to this instance.
* @returns {Netdump}
*/
async newNetdump () {
return new Netdump(this)
}
/**
* Download specified pcap file. If pcapFile is not given or is invalid, default to netdump.pcap
* @param {string} pcapFile - Which pcap file you want to download
* @param {boolean} [asStream] - If true, return pcap as stream instead of buffer.
* @returns {Promise<Buffer | NodeJS.ReadableStream>}
* @example
* const pcap = await instance.downloadPcap();
* console.log(pcap.toString());
*/
async downloadPcap (pcapFile, asStream) {
const availablePcaps = {
networkMonitor: { preAuth: '/networkMonitorPcap-authorize', pcapFile: 'networkMonitor.pcap' },
netdump: { preAuth: '/netdumpPcap-authorize', pcapFile: 'netdump.pcap' }
}
const pcap = (typeof pcapFile === 'string' && availablePcaps[pcapFile]) || availablePcaps.netdump
const token = await this._fetch(pcap.preAuth, { method: 'POST' })
const response = await fetchApi(this.project, `/preauthed/${token.token}/${pcap.pcapFile}`, { response: 'raw' })
if (asStream) {
return response.body
}
return await response.buffer()
}
/**
* Return an {@link NetworkMonitor} connected to this instance. Calling this
* method multiple times will reuse the same agent connection.
* @returns {Promise<NetworkMonitor>}
*/
async networkMonitor () {
if (!this._netmon) this._netmon = await this.newNetworkMonitor()
return this._netmon
}
async netmonEndpoint () {
// Extra while loop to avoid races where info.netmon gets unset again before we wake back up.
while (!this.info.netmon) await this._waitFor(() => !!this.info.netmon)
// We want to avoid a situation where we were not listening for updates, and the info we have is stale (from last boot),
// and the instance has started again but this time with no agent info yet or new agent info. Therefore, we can use
// cached if only if it's recent.
if (new Date().getTime() - this.infoDate.getTime() > 2 * this.project.updater.updateInterval) {
try {
await this.update()
} catch (err) {
if (err.stack.includes('500 Internal Server Error')) {
return undefined
}
}
}
return this.project.api + '/agent/' + this.info.netmon.info
}
/**
* Create a new {@link NetworkMonitor} connection to this instance.
* @returns {NetworkMonitor}
*/
async newNetworkMonitor () {
return new NetworkMonitor(this)
}
/**
* Returns a bidirectional node stream for this instance's serial console.
* @return {WebSocket-Stream}
* @example
* const consoleStream = await instance.console();
* consoleStream.pipe(process.stdout);
*/
async console () {
const { url } = await this._fetch('/console')
return wsstream(url, ['binary'])
}
/**
* Waits for a specified line on console.
* @example
* await instance.waitForLineOnConsole(line)
*/
async waitForLineOnConsole (line) {
const stream = await this.console()
await stream
.pipe(split())
.on('data', l => {
if (l === line) {
stream.destroy()
}
})
.on('end', () => {
})
}
/**
* Send an input to this instance.
* @param {Input} input - The input to send.
* @see Input
* @example
* await instance.sendInput(I.pressRelease('home'));
*/
async sendInput (input) {
await this._fetch('/input', { method: 'POST', json: input.points })
}
/**
* @typedef {object} StartOptions
* @property {boolean} sockcap - Start the sockcap add-on extension if loaded
* @property {boolean} paused - Start the instance in a paused state
*/
/**
* Start this instance.
* @param {StartOptions} options
* @example
* await instance.start({
* sockcap: true,
* paused: true
* });
*/
async start (options) {
await this._fetch('/start', { method: 'POST' }, options)
}
/**
* Stop this instance.
* @example
* await instance.stop();
*/
async stop () {
await this._fetch('/stop', { method: 'POST' })
}
/**
* Pause this instance
* @example
* await instance.pause();
*/
async pause () {
await this._fetch('/pause', { method: 'POST' })
}
/**
* Unpause this instance
* @example
* await instance.unpause();
*/
async unpause () {
await this._fetch('/unpause', { method: 'POST' })
}
/**
* Reboot this instance.
* @example
* await instance.reboot();
*/
async reboot () {
await this._fetch('/reboot', { method: 'POST' })
await this.waitForTaskState('rebooting')
await this.waitForTaskState('none')
}
/**
* Restore instance from backup
* @param {string} [password] - Password for encrypted backups
* @example
* await instance.restoreBackup();
*/
async restoreBackup (password) {
await this._fetch('/restoreBackup', {
method: 'POST',
json: { password }
})
await this.waitForUserTask(null)
}
/**
* @typedef {object} UpgradeOptions
* @property {string} os - The target iOS version
* @property {string} osbuild - Specific build identifier (optional)
*/
/**
* Upgrade the iOS version of this instance.
* @param {UpgradeOptions} options
* @example
* await instance.upgrade({
* os: '16.1',
* osbuild: '20B79'
* });
*/
async upgrade (options) {
await this._fetch('/upgrade', {
method: 'POST',
json: Object.assign({}, options)
})
await this.waitForState('updating')
}
/**
* Destroy this instance.
* @example <caption>delete all instances of the project</caption>
* let instances = await project.instances();
* instances.forEach(instance => {
* instance.destroy();
* });
*/
async destroy () {
await this._fetch('', { method: 'DELETE' })
}
/**
* Send a message to an jailbroken iOS device
* @example
* await instance.message('123','321');
*
* @param {string} number - phone number to receive the message from
* @param {string} message - message to receive
*/
async message (sender, message) {
await this._fetch('/message', {
method: 'POST',
json: { number: sender, message: message }
})
}
/**
* Send a raw message to an jailbroken iOS device
* @example
* await instance.messageRaw('AAAAAAAAAAAAAAAAA');
*
* @param {string} data - hex encoded data to send
*/
async messageRaw (data) {
await this._fetch('/message', { method: 'POST', json: { raw: data } })
}
/**
* Get CoreTrace Thread List
* @return {Promise<ThreadInfo[]>}
* @example
* let procList = await instance.getCoreTraceThreadList();
* for (let p of procList) {
* console.log(p.pid, p.kernelId, p.name);
* for (let t of p.threads) {
* console.log(t.tid, t.kernelId);
* }
* }
*/
async getCoreTraceThreadList () {
return await this._fetch('/strace/thread-list', { method: 'GET' })
}
/**
* Add List of PIDs/Names/TIDs to CoreTrace filter
* @param {integer[]} pids - array of process IDs to filter
* @param {string[]} names - array of process names to filter
* @param {integer[]} tids - array of thread IDs to filter
* @example
* await instance.setCoreTraceFilter([111, 222], ["proc_name"], [333]);
*/
async setCoreTraceFilter (pids, names, tids) {
let filter = []
if (pids.length) {
filter = filter.concat(
pids.map(pid => {
return { trait: 'pid', value: pid.toString() }
})
)
}
if (names.length) {
filter = filter.concat(
names.map(name => {
return { trait: 'name', value: name }
})
)
}
if (tids.length) {
filter = filter.concat(
tids.map(tid => {
return { trait: 'tid', value: tid.toString() }
})
)
}
await this._fetch('', { method: 'PATCH', json: { straceFilter: filter } })
}
/**
* Clear CoreTrace filter
* @example
* await instance.clearCoreTraceFilter();
*/
async clearCoreTraceFilter () {
await this._fetch('', { method: 'PATCH', json: { straceFilter: [] } })
}
/**
* Start CoreTrace
* @example
* await instance.startCoreTrace();
*/
async startCoreTrace () {
await this._fetch('/strace/enable', { method: 'POST' })
if (this.info.coreTrace) {
await this._waitFor(() => this.info.coreTrace.enabled === true)
}
}
/**
* Stop CoreTrace
* @example
* await instance.stopCoreTrace();
*/
async stopCoreTrace () {
await this._fetch('/strace/disable', { method: 'POST' })
if (this.info.coreTrace) {
await this._waitFor(() => this.info.coreTrace.enabled === false)
}
}
/**
* Download CoreTrace Log
* @example
* let trace = await instance.downloadCoreTraceLog();
* console.log(trace.toString());
*/
async downloadCoreTraceLog () {
const token = await this._fetch('/strace-authorize', { method: 'GET' })
const response = await fetchApi(this.project, '/preauthed/' + token.token + '/coretrace.log', {
response: 'raw'
})
return await response.buffer()
}
/**
* Clean CoreTrace log
* @example
* await instance.clearCoreTraceLog();
*/
async clearCoreTraceLog () {
await this._fetch('/strace', { method: 'DELETE' })
}
/**
* Returns a bidirectional node stream for this instance's frida console.
* @return {WebSocket-Stream}
* @example
* const consoleStream = await instance.fridaConsole();
* consoleStream.pipe(process.stdout);
*/
async fridaConsole () {
const { url } = await this._fetch('/console?type=frida')
const fridaConsole = wsstream(url, ['binary'])
await new Promise(resolve => {
fridaConsole.socket.on('open', () => {
resolve()
})
})
return fridaConsole
}
/**
* Execute FRIDA script by name
* @param {string} filePath - path to FRIDA script
* @example
* await instance.executeFridaScript("/data/corellium/frida/scripts/script.js");
*/
async executeFridaScript (filePath) {
const fridaConsoleStream = await this.fridaConsole()
fridaConsoleStream.socket.on('close', function () {
fridaConsoleStream.destroy()
})
fridaConsoleStream.socket.send('%load ' + filePath + '\n', null, () =>
fridaConsoleStream.socket.close()
)
fridaConsoleStream.socket.close()
}
/**
* Takes a screenshot of this instance's screen. Returns a Buffer containing image data.
* @param {Object} options
* @param {string} [options.format=png] - Either `png` or `jpg`.
* @param {int} [options.scale=1] - The image scale. Specifying 2 would result
* in an image with half the instance's native resolution. This is useful
* because smaller images are quicker to capture and transmit over the
* network.
* @example
* const screenshot = await instance.takeScreenshot();
* fs.writeFileSync('screenshot.png', screenshot);
*/
async takeScreenshot (options) {
const { format = 'png', scale = 1 } = options || {}
const res = await this._fetch(`/screenshot.${format}?scale=${scale}`, {
response: 'raw'
})
if (res.buffer) return await res.buffer()
// node
else return await res.blob() // browser
}
/**
* Enable exposing a port for connecting to VM.
* For iOS, this would mean ssh, for Android, adb access.
*/
async enableExposedPort () {
await this._fetch('/exposeport/enable', { method: 'POST' })
}
/**
* Disable exposing a port for connecting to VM.
* For iOS, this would mean ssh, for Android, adb access.
*/
async disableExposedPort () {
await this._fetch('/exposeport/disable', { method: 'POST' })
}
async update () {
this.receiveUpdate(await this._fetch(''))
}
receiveUpdate (info) {
this.infoDate = new Date()
// one way of checking object equality
if (JSON.stringify(info) !== JSON.stringify(this.info)) {
this.info = info
/**
* Fired when a property of an instance changes, such as its name or its state.
* @event Instance#change
* @example
* instance.on('change', () => {
* console.log(instance.id, instance.name, instance.state);
* });
*/
this.emit('change')
if (info.panicked) {
/**
* Fired when an instance panics. The panic information can be retrieved with {@link Instance#panics}.
* @event Instance#panic
* @example
* instance.on('panic', async () => {
* try {
* console.log('Panic detected!');
* // get the panic log(s)
* console.log(await instance.panics());
* // Download the console log.
* console.log(await instance.consoleLog());
* // Clear the panic log.
* await instance.clearPanics();
* // Reboot the instance.
* await instance.reboot();
* } catch (e) {
* // handle the error somehow to avoid an unhandled promise rejection
* }
* });
*/
this.emit('panic')
}
}
}
/**
* Wait for ...
* @param {function} reporterFn - Called with instance information (optional)
*/
async _waitFor (callback, reporterFn = null) {
await this.update()
return new Promise(resolve => {
const change = () => {
let done
try {
done = callback()
} catch (e) {
done = false
}
if (typeof reporterFn === 'function') {
reporterFn(this.info)
}
if (done) {
this.removeListener('change', change)
resolve()
}
}
this.on('change', change)
change()
})
}
/**
* Wait for the instance to finish upgrading.
* @param {function} reporterFn - Called with instance information (optional)
* @example <caption>Wait for VM to finish OS upgrade</caption>
* instance.finishUpgrade();
*/
async finishUpgrade (reporterFn = null) {
await this._waitFor(() => this.state !== 'updating', reporterFn)
}
/**
* Wait for the instance to finish restoring and start its first boot.
* @param {function} reporterFn - Called with instance information (optional)
* @example <caption>Wait for VM to finish restore</caption>
* instance.finishRestore();
*/
async finishRestore (reporterFn = null) {
await this._waitFor(() => this.state !== 'creating', reporterFn)
}
/**
* Wait for the instance to enter the given state.
* @param {string} state - state to wait
* @param {function} reporterFn - Called with instance information (optional)
* @example <caption>Wait for VM to be ON</caption>
* instance.waitForState('on');
*/
async waitForState (state, reporterFn = null) {
await this._waitFor(() => this.state === state, reporterFn)
}
/**
* Wait for the instance task to enter the given state.
* @param {function} reporterFn - Called with instance information (optional)
* @param {string} taskName
*/
async waitForTaskState (taskName, reporterFn = null) {
await this._waitFor(() => this.taskState === taskName, reporterFn)
}
/**
* Wait for the instance user task name to be a given state.
* @param {function} reporterFn - Called with instance information (optional)
* @param {string} userTaskName
*/
async waitForUserTask (userTaskName, reporterFn = null) {
await this._waitFor(() => {
if (!userTaskName) {
return !this.userTask
} else {
return this.userTask.name === userTaskName
}
}, reporterFn)
}
async _fetch (endpoint = '', options = {}) {
return await fetchApi(this.project, `/instances/${this.id}${endpoint}`, options)
}
/**
* Delete Iot Firmware that is attached to an instance
* @param {FirmwareImage} firmwareImage
*/
async deleteIotFirmware (firmwareImage) {
return await this.deleteImage(firmwareImage)
}
/**
* Delete kernel that is attached to an instance
* @param {KernelImage} kernelImage
*/
async deleteKernel (kernelImage) {
return await this.deleteImage(kernelImage)
}
/**
* Delete an image that is attached to an instance
* @param {Image | KernelImage | FirmwareImage} kernelImage
*/
async deleteImage (image) {
return await fetchApi(this, `/images/${image.id}`, {
method: 'DELETE'
})
}
/**
* Get all images attached to this instance with optional type
* @param {string} type - the type of image being uploaded ie. kernel, ramdisk, devicetree, or backup
*/
async getImages (type) {
const images = (await Images.listImagesMetaData(this.project)).filter(
i => i.instance === this.id
)
return type ? images.filter(i => i.type === type) : images
}
/**
* Upload a partition (e.g. flash) to an instance.
*
* @param {string} filePath - The path on the local file system to get the firmware file.
* @param {string} name - The name of the partition to load this file to.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<PartitionImage>}
*/
async uploadPartition (filePath, name, progress) {
return await this.compressAndUploadImage('partition', filePath, name, progress)
}
/**
* Add a custom IoT firmware image to a project for use in creating new instances.
*
* @param {string} filePath - The path on the local file system to get the firmware file.
* @param {string} name - The name of the file to identify the file on the server. Usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<FirmwareImage>}
*/
async uploadIotFirmware (filePath, name, progress) {
return await this.uploadKernel(filePath, name, progress)
}
/**
* compress an image and upload it to an instance
*
* @param {string} type - the type of image being uploaded ie. kernel, ramdisk, or devicetree
* @param {string} filePath - The path on the local file system to get the file.
* @param {string} name - The name of the file to identify the file on the server. Usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<{ id: string, name: string }>}
*/
async compressAndUploadImage (type, filePath, name, progress) {
let tmpfile = null
const data = await util.promisify(fs.readFile)(filePath)
tmpfile = await compress(data, name)
const uploadedImage = await this.uploadImage(type, tmpfile, name, progress)
if (tmpfile) {
fs.unlinkSync(tmpfile)
}
return { id: uploadedImage.id, name: uploadedImage.name }
}
/**
* Add a kernel image to an instance
*
* @param {string} filePath - The path on the local file system to get the kernel file.
* @param {string} name - The name of the file to identify the file on the server. Usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<KernelImage>}
*/
async uploadKernel (filePath, name, progress) {
await this.compressAndUploadImage('kernel', filePath, name, progress)
}
/**
* Add a ram disk image to an instance
*
* @param {string} filePath - The path on the local file system to get the ram disk file.
* @param {string} name - The name of the file to identify the file on the server. Usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<RamDiskImage>}
*/
async uploadRamDisk (filePath, name, progress) {
await this.compressAndUploadImage('ramdisk', filePath, name, progress)
}
/**
* Add a device tree image to an instance.
*
* @param {string} filePath - The path on the local file system to get the device tree file.
* @param {string} name - The name of the file to identify the file on the server. Usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<DeviceTreeImage>}
*/
async uploadDeviceTree (filePath, name, progress) {
await this.compressAndUploadImage('devicetree', filePath, name, progress)
}
/**
* Add an image to the instance. These images may be removed at any time and are meant to facilitate creating a new Instance with images.
*
* @param {string} type - E.g. fw for the main firmware image.
* @param {string} filePath - The path on the local file system to get the file.
* @param {string} name - The name of the file to identify the file on the server. Usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<{ id: string, name: string }>}
*/
async uploadImage (type, filePath, name, progress) {
const imageId = uuidv4()
const token = await this.project.getToken()
const url =
this.project.api +
'/instances/' +
encodeURIComponent(this.id) +
'/image-upload/' +
encodeURIComponent(type) +
'/' +
encodeURIComponent(imageId) +
'/' +
encodeURIComponent(name)
await uploadFile(token, url, filePath, progress)
return { id: imageId, name }
}
/**
* Return a {@link WebPlayer} session tied to this instance.
* Calling this method multiple times will reuse the same webplayer.
*
* @param {object} features - The enabled frontend feature set for this session
* @param {object} permissions - The endpoint permissions for this session
* @param {number?} expiresIn - Number of seconds until the token expires (default: 15 minutes)
* @returns {Promise<WebPlayer | null>}
*
* @example
* let webplayer = instance.webplayer(features, permissions);
*/
async webplayer (features, permissions, expiresIn = 15 * 60) {
if (this._webplayer === null) {
this._webplayer = new WebPlayer(this.project, this.id, features, permissions)
await this._webplayer._createSession(expiresIn, () => {
console.log('Instance destroy WebPlayer')
this._webplayer = null
})
}
return this._webplayer
}
}
module.exports = Instance