UNPKG

8.07 kBJavaScriptView Raw
1import { spawn, spawnSync } from 'node:child_process';
2import { Plist, ValueDict, ValueArray, ValueString, ValueBoolean } from '@shockpkg/plist-dom';
3/**
4 * Mounter object.
5 */
6export class Mounter {
7 /**
8 * The path to hdiutil.
9 */
10
11 /**
12 * Mounter constructor.
13 *
14 * @param options Options object.
15 */
16 constructor(options = null) {
17 this.hdiutil = (options ? options.hdiutil : null) || 'hdiutil';
18 }
19
20 /**
21 * Attach a disk image.
22 *
23 * @param file Path to disk image.
24 * @param options Options object.
25 * @param ejectOnExit Eject on exit options, or null.
26 * @returns Info object.
27 */
28 async attach(file, options = null, ejectOnExit = null) {
29 const devices = await this._runAttach(this._argsAttach(file, options));
30 const {
31 eject,
32 ejectSync
33 } = this._createEjects(devices);
34 return {
35 devices,
36 eject,
37 ejectSync
38 };
39 }
40
41 /**
42 * Attach a disk image.
43 *
44 * @param file Path to disk image.
45 * @param options Options object.
46 * @param ejectOnExit Eject on exit options, or null.
47 * @returns Info object.
48 */
49 attachSync(file, options = null, ejectOnExit = null) {
50 // eslint-disable-next-line no-sync
51 const devices = this._runAttachSync(this._argsAttach(file, options));
52 const {
53 eject,
54 ejectSync
55 } = this._createEjects(devices);
56 return {
57 devices,
58 eject,
59 ejectSync
60 };
61 }
62
63 /**
64 * Eject a disk image.
65 *
66 * @param file Path to device file or volume mount point.
67 * @param options Options object.
68 */
69 async eject(file, options = null) {
70 await this._runEject(this._argsEject(file, options));
71 }
72
73 /**
74 * Eject a disk image.
75 *
76 * @param file Path to device file or volume mount point.
77 * @param options Options object.
78 */
79 ejectSync(file, options = null) {
80 // eslint-disable-next-line no-sync
81 this._runEjectSync(this._argsEject(file, options));
82 }
83
84 /**
85 * Create args for attach.
86 *
87 * @param file Path to disk image.
88 * @param options Options object.
89 * @returns Argument list.
90 */
91 _argsAttach(file, options = null) {
92 const args = ['attach', '-plist'];
93 if (options) {
94 if (options.readonly) {
95 args.push('-readonly');
96 }
97 if (options.nobrowse) {
98 args.push('-nobrowse');
99 }
100 }
101 args.push(this._fileArg(file));
102 return args;
103 }
104
105 /**
106 * Create args for eject.
107 *
108 * @param file Path to device file or volume mount point.
109 * @param options Options object.
110 * @returns Argument list.
111 */
112 _argsEject(file, options = null) {
113 const args = ['eject'];
114 if (options && options.force) {
115 args.push('-force');
116 }
117 args.push(this._fileArg(file));
118 return args;
119 }
120
121 /**
122 * Run hdiutil attach command, returning the devices list on success.
123 *
124 * @param args CLI args.
125 * @returns Devices list.
126 */
127 async _runAttach(args) {
128 const stdouts = [];
129 const proc = spawn(this.hdiutil, args);
130 proc.stdout.on('data', data => {
131 stdouts.push(data);
132 });
133 const code = await new Promise((resolve, reject) => {
134 proc.once('exit', resolve);
135 proc.once('error', reject);
136 });
137 if (code) {
138 throw new Error(`Attach failed: hdiutil exit code: ${code}`);
139 }
140 return this._parseDevices(Buffer.concat(stdouts).toString());
141 }
142
143 /**
144 * Run hdiutil attach command, returning the devices list on success.
145 *
146 * @param args CLI args.
147 * @returns Devices list.
148 */
149 _runAttachSync(args) {
150 const {
151 status,
152 error,
153 stdout
154 } = spawnSync(this.hdiutil, args);
155 if (error) {
156 throw error;
157 }
158 if (status) {
159 throw new Error(`Attach failed: hdiutil exit code: ${status}`);
160 }
161 return this._parseDevices(stdout.toString());
162 }
163
164 /**
165 * Run hdiutil eject command.
166 *
167 * @param args CLI args.
168 */
169 async _runEject(args) {
170 const proc = spawn(this.hdiutil, args);
171 const status = await new Promise((resolve, reject) => {
172 proc.once('exit', resolve);
173 proc.once('error', reject);
174 });
175 if (status) {
176 throw new Error(`Eject failed: hdiutil exit code: ${status}`);
177 }
178 }
179
180 /**
181 * Run hdiutil eject command.
182 *
183 * @param args CLI args.
184 */
185 _runEjectSync(args) {
186 const {
187 status,
188 error
189 } = spawnSync(this.hdiutil, args);
190 if (error) {
191 throw error;
192 }
193 if (status) {
194 throw new Error(`Eject failed: hdiutil exit code: ${status}`);
195 }
196 }
197
198 /**
199 * Create file argument from file path.
200 *
201 * @param file File path.
202 * @returns A path for use as argument.
203 */
204 _fileArg(file) {
205 // Make sure it will not be recognized as option argument.
206 return file.startsWith('-') ? `./${file}` : file;
207 }
208
209 /**
210 * Parse devices plist into devices list.
211 *
212 * @param xml XML plist.
213 * @returns Devices list.
214 */
215 _parseDevices(xml) {
216 const plist = new Plist();
217 plist.fromXml(xml);
218 const systemEntities = plist.valueCastAs(ValueDict).getValue('system-entities').castAs(ValueArray);
219 const r = [];
220 for (const value of systemEntities.value) {
221 const dict = value.castAs(ValueDict);
222 const devEntry = dict.getValue('dev-entry').castAs(ValueString).value;
223 const potentiallyMountable = dict.getValue('potentially-mountable').castAs(ValueBoolean).value;
224 const contentHint = dict.get('content-hint');
225 const unmappedContentHint = dict.get('unmapped-content-hint');
226 const volumeKind = dict.get('volume-kind');
227 const mountPoint = dict.get('mount-point');
228 const device = {
229 devEntry,
230 potentiallyMountable
231 };
232 if (contentHint) {
233 device.contentHint = contentHint.castAs(ValueString).value;
234 }
235 if (unmappedContentHint) {
236 device.unmappedContentHint = unmappedContentHint.castAs(ValueString).value;
237 }
238 if (volumeKind) {
239 device.volumeKind = volumeKind.castAs(ValueString).value;
240 }
241 if (mountPoint) {
242 device.mountPoint = mountPoint.castAs(ValueString).value;
243 }
244 r.push(device);
245 }
246 return r;
247 }
248
249 /**
250 * Find the root device, null on empty list.
251 *
252 * @param devices Device list.
253 * @returns Root device or null if an empty list.
254 */
255 _findRootDevice(devices) {
256 let r = null;
257 for (const device of devices) {
258 if (r === null || r.devEntry.length > device.devEntry.length) {
259 r = device;
260 }
261 }
262 return r;
263 }
264
265 /**
266 * Create ejects callback from a list of devices.
267 *
268 * @param devices Device list.
269 * @param ejectOnExit Eject on exit options, or null.
270 * @returns Callback function.
271 */
272 _createEjects(devices, ejectOnExit = null) {
273 // Find the root device, to use to eject (none possible in theory).
274 let devEntry = this._findRootDevice(devices)?.devEntry;
275 let shutdown = null;
276 const info = {
277 /**
278 * The eject callback function.
279 *
280 * @param options Eject options.
281 */
282 eject: async (options = null) => {
283 if (devEntry) {
284 await this.eject(devEntry, options);
285 devEntry = '';
286 if (shutdown) {
287 process.off('exit', shutdown);
288 }
289 }
290 },
291 /**
292 * The eject callback function.
293 *
294 * @param options Eject options.
295 */
296 ejectSync: (options = null) => {
297 if (devEntry) {
298 // eslint-disable-next-line no-sync
299 this.ejectSync(devEntry, options);
300 devEntry = '';
301 if (shutdown) {
302 process.off('exit', shutdown);
303 }
304 }
305 }
306 };
307 if (ejectOnExit) {
308 /**
309 * Attempt to auto-eject on normal shutdown.
310 * Does not catch signals (no clean way in a library).
311 * Users can explicitly call process.exit() on signals to use this.
312 */
313 shutdown = () => {
314 // eslint-disable-next-line no-sync
315 info.ejectSync(ejectOnExit);
316 };
317 process.once('exit', shutdown);
318 }
319 return info;
320 }
321}
322//# sourceMappingURL=mounter.mjs.map
\No newline at end of file