UNPKG

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