@ronomon/opened
Version:
Check if a file is open in another application on Windows, macOS and Linux.
254 lines (234 loc) • 7.12 kB
JavaScript
;
var Node = {
child: require('child_process'),
fs: require('fs'),
path: require('path'),
process: process
};
var Queue = require('@ronomon/queue');
function assertFunction(key, value) {
if (typeof value !== 'function') {
throw new Error(key + ' must be a function');
}
}
function assertPath(key, value) {
if (typeof value !== 'string') {
throw new Error(key + ' must be a string');
}
if (value.length === 0) {
throw new Error(key + ' must not be empty');
}
if (value.indexOf('\u0000') !== -1) {
throw new Error(key + ' must be a string without null bytes');
}
if (value.indexOf(assertPathBadSep) !== -1) {
throw new Error(key + ' must be a string without ' + assertPathBadSepName);
}
}
if (Node.process.platform === 'win32') {
var assertPathBadSep = '\/';
var assertPathBadSepName = 'forward slashes';
} else {
var assertPathBadSep = '\\';
var assertPathBadSepName = 'backslashes';
}
function assertPaths(key, value) {
if (!value || value.constructor !== Array) {
throw new Error(key + ' must be an array');
}
for (var index = 0, length = value.length; index < length; index++) {
assertPath('path at index ' + index, value[index]);
}
}
function pathBuffer(path) {
var pathLong = Node.path._makeLong(path);
var buffer = Buffer.alloc(Buffer.byteLength(pathLong, 'utf-8') + 1);
buffer.write(pathLong, 0, buffer.length - 1, 'utf-8');
buffer[buffer.length - 1] = 0;
if (buffer.indexOf(0) !== buffer.length - 1) {
throw new Error('path must be a string without null bytes');
}
return buffer;
}
var Unix = {};
Unix.file = function(path, end) {
var self = this;
assertPath('path', path);
assertFunction('end', end);
self.files([path],
function(error, files) {
if (error) return end(error);
end(undefined, files[path]);
}
);
};
Unix.files = function(paths, end) {
var self = this;
assertPaths('paths', paths);
assertFunction('end', end);
var files = {};
var queue = new Queue(1); // Concurrency yields no improvement with lsof.
queue.onData = function(paths, end) {
var command = 'lsof';
var args = ['-F', 'n', '--'].concat(paths);
var options = {
encoding: 'utf-8',
maxBuffer: 2 * 1024 * 1024
};
Node.child.execFile(command, args, options,
function(error, stdout, stderr) {
// lsof returns an error and a status code of 1 if a file is not open:
if (error && error.code === 1 && stderr.length === 0) error = undefined;
if (error) {
if (/No such file or directory/i.test(stderr)) {
error.code = 'ENOENT';
}
return end(error);
}
var lines = stdout.split('\n');
for (var index = 0, length = lines.length; index < length; index++) {
var line = lines[index];
if (line[0] != 'n') continue;
var candidate = self.unescape(line.slice(1));
if (files.hasOwnProperty(candidate)) files[candidate] = true;
}
end();
}
);
};
queue.onEnd = function(error) {
if (error) return end(error);
end(undefined, files);
};
// lsof performs in constant time regardless of the number of paths.
// We therefore batch calls to lsof to improve latency.
// We must be careful however not to exceed any limits on command length.
// 32 paths at 32768 bytes per path requires just over 1 MB.
// We assume we are safe up to 2 MB.
// See: xargs --show-limits
var batch = [];
for (var index = 0, length = paths.length; index < length; index++) {
var path = paths[index];
if (files.hasOwnProperty(path)) continue;
files[path] = false;
batch.push(path);
if (batch.length === 32) {
queue.push(batch);
batch = [];
}
}
if (batch.length) queue.push(batch);
queue.end();
};
Unix.unescape = function(sourceString) {
var self = this;
var source = Buffer.from(sourceString, 'utf-8');
var target;
var targetIndex;
var sourceIndex = 0;
var sourceLength = source.length;
while (sourceIndex < sourceLength) {
if (source[sourceIndex] === 92 && sourceIndex + 1 < sourceLength) { // "\\"
if (!target) {
target = Buffer.alloc(sourceLength);
targetIndex = source.copy(target, 0, 0, sourceIndex);
}
sourceIndex++;
target[targetIndex++] = self.unescapeTable[source[sourceIndex++]];
} else if (target) {
target[targetIndex++] = source[sourceIndex++];
} else {
sourceIndex++;
}
}
if (target) {
return target.toString('utf-8', 0, targetIndex);
} else {
return sourceString;
}
};
Unix.unescapeTable = (function() {
var table = Buffer.alloc(256);
for (var code = 0; code < 256; code++) table[code] = code;
table['b'.charCodeAt(0)] = '\b'.charCodeAt(0);
table['f'.charCodeAt(0)] = '\f'.charCodeAt(0);
table['t'.charCodeAt(0)] = '\t'.charCodeAt(0);
table['n'.charCodeAt(0)] = '\n'.charCodeAt(0);
table['r'.charCodeAt(0)] = '\r'.charCodeAt(0);
return table;
})();
var Windows = {};
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382.aspx
Windows.codes = {
1: 'EISDIR', // ERROR_INVALID_FUNCTION
2: 'ENOENT', // ERROR_FILE_NOT_FOUND
3: 'ENOENT', // ERROR_PATH_NOT_FOUND
4: 'EMFILE', // ERROR_TOO_MANY_OPEN_FILES
5: 'EPERM', // ERROR_ACCESS_DENIED
6: 'EBADF', // ERROR_INVALID_HANDLE
8: 'ENOMEM', // ERROR_NOT_ENOUGH_MEMORY
14: 'ENOMEM', // ERROR_OUTOFMEMORY
15: 'ENOENT', // ERROR_INVALID_DRIVE
32: 'ERROR_SHARING_VIOLATION',
33: 'ERROR_LOCK_VIOLATION'
};
if (Node.process.platform === 'win32') {
Windows.binding = require('./binding.node');
}
Windows.file = function(path, end) {
var self = this;
assertPath('path', path);
assertFunction('end', end);
self.binding.opened(pathBuffer(path),
function(result) {
if (result === 0) return end(undefined, false);
if (self.codes.hasOwnProperty(result)) {
var code = self.codes[result];
} else {
var code = 'ENOSYS';
}
if (
code === 'ERROR_SHARING_VIOLATION' ||
code === 'ERROR_LOCK_VIOLATION'
) {
return end(undefined, true);
}
var error = new Error(code + ': -' + result + ', opened(' + path + ')');
error.code = code;
end(error);
}
);
};
Windows.files = function(paths, end) {
var self = this;
assertPaths('paths', paths);
assertFunction('end', end);
var files = {};
var queue = new Queue(4);
queue.onData = function(path, end) {
self.file(path,
function(error, opened) {
if (error) return end(error);
if (opened) files[path] = true;
end();
}
);
};
queue.onEnd = function(error) {
if (error) return end(error);
end(undefined, files);
};
for (var index = 0, length = paths.length; index < length; index++) {
var path = paths[index];
if (files.hasOwnProperty(path)) continue;
files[path] = false;
queue.push(path);
}
queue.end();
};
if (Node.process.platform === 'win32') {
module.exports = Windows;
} else {
module.exports = Unix;
}
// S.D.G.