Compare commits

..

2 Commits

Author SHA1 Message Date
Nick O'Leary
afb06e8c9a Update packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/util.js 2023-09-05 15:11:47 +01:00
Nick O'Leary
2c1274ff76 Ensure storage/util.writeFile handles concurrent write attempts 2023-09-05 15:09:11 +01:00
3 changed files with 57 additions and 42 deletions

View File

@@ -302,21 +302,11 @@ RED.view = (function() {
return api
})()
const isMac = RED.utils.getBrowserInfo().os === 'mac'
// 'Control' is the main modifier key for mouse actions. On Windows,
// that is the standard Ctrl key. On Mac that is the Cmd key.
function isControlPressed (event) {
return (isMac && event.metaKey) || (!isMac && event.ctrlKey)
}
function init() {
chart = $("#red-ui-workspace-chart");
chart.on('contextmenu', function(evt) {
if (RED.view.DEBUG) {
console.warn("contextmenu", { mouse_mode, event: d3.event });
}
mouse_mode = RED.state.DEFAULT
evt.preventDefault()
evt.stopPropagation()
RED.contextMenu.show({
@@ -1200,7 +1190,7 @@ RED.view = (function() {
lasso = null;
}
if (d3.event.touches || d3.event.button === 0) {
if ((mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) && isControlPressed(d3.event) && !(d3.event.altKey || d3.event.shiftKey)) {
if ((mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) && (d3.event.metaKey || d3.event.ctrlKey) && !(d3.event.altKey || d3.event.shiftKey)) {
// Trigger quick add dialog
d3.event.stopPropagation();
clearSelection();
@@ -1210,7 +1200,7 @@ RED.view = (function() {
clickedGroup = clickedGroup || RED.nodes.group(drag_lines[0].node.g)
}
showQuickAddDialog({ position: point, group: clickedGroup });
} else if (mouse_mode === 0 && !isControlPressed(d3.event)) {
} else if (mouse_mode === 0 && !(d3.event.metaKey || d3.event.ctrlKey)) {
// CTRL not being held
if (!d3.event.altKey) {
// ALT not held (shift is allowed) Trigger lasso
@@ -3550,7 +3540,7 @@ RED.view = (function() {
d3.event.preventDefault()
document.getSelection().removeAllRanges()
if (d.type != "subflow") {
if (/^subflow:/.test(d.type) && isControlPressed(d3.event)) {
if (/^subflow:/.test(d.type) && (d3.event.ctrlKey || d3.event.metaKey)) {
RED.workspaces.show(d.type.substring(8));
} else {
RED.editor.edit(d);
@@ -3714,12 +3704,12 @@ RED.view = (function() {
d.type !== 'junction'
lastClickNode = mousedown_node;
if (d.selected && isControlPressed(d3.event)) {
if (d.selected && (d3.event.ctrlKey||d3.event.metaKey)) {
mousedown_node.selected = false;
movingSet.remove(mousedown_node);
} else {
if (d3.event.shiftKey) {
if (!isControlPressed(d3.event)) {
if (!(d3.event.ctrlKey||d3.event.metaKey)) {
clearSelection();
}
var clickPosition = (d3.event.offsetX/scaleFactor - mousedown_node.x)
@@ -3888,10 +3878,10 @@ RED.view = (function() {
}
mousedown_link = d;
if (!isControlPressed(d3.event)) {
if (!(d3.event.metaKey || d3.event.ctrlKey)) {
clearSelection();
}
if (isControlPressed(d3.event)) {
if (d3.event.metaKey || d3.event.ctrlKey) {
if (!selectedLinks.has(mousedown_link)) {
selectedLinks.add(mousedown_link);
} else {
@@ -3906,7 +3896,7 @@ RED.view = (function() {
redraw();
focusView();
d3.event.stopPropagation();
if (!mousedown_link.link && movingSet.length() === 0 && (d3.event.touches || d3.event.button === 0) && selectedLinks.length() === 1 && selectedLinks.has(mousedown_link) && isControlPressed(d3.event)) {
if (!mousedown_link.link && movingSet.length() === 0 && (d3.event.touches || d3.event.button === 0) && selectedLinks.length() === 1 && selectedLinks.has(mousedown_link) && (d3.event.metaKey || d3.event.ctrlKey)) {
d3.select(this).classed("red-ui-flow-link-splice",true);
var point = d3.mouse(this);
var clickedGroup = getGroupAt(point[0],point[1]);
@@ -3987,7 +3977,7 @@ RED.view = (function() {
);
lastClickNode = g;
if (g.selected && isControlPressed(d3.event)) {
if (g.selected && (d3.event.ctrlKey||d3.event.metaKey)) {
selectedGroups.remove(g);
d3.event.stopPropagation();
} else {

View File

@@ -71,30 +71,30 @@ function readFile(path,backupPath,emptyResponse,type) {
});
}
const writeFileLocks = {}
module.exports = {
/**
* Write content to a file using UTF8 encoding.
* This forces a fsync before completing to ensure
* the write hits disk.
*/
writeFile: function(path,content,backupPath) {
var backupPromise;
if (backupPath && fs.existsSync(path)) {
backupPromise = fs.copy(path,backupPath);
} else {
backupPromise = Promise.resolve();
writeFile: async function(path,content,backupPath) {
if (!writeFileLocks[path]) {
writeFileLocks[path] = Promise.resolve()
}
const dirname = fspath.dirname(path);
const tempFile = `${path}.$$$`;
return backupPromise.then(() => {
if (backupPath) {
const result = writeFileLocks[path].then(async () => {
var backupPromise;
if (backupPath && fs.existsSync(path)) {
await fs.copy(path,backupPath);
log.trace(`utils.writeFile - copied ${path} TO ${backupPath}`)
}
return fs.ensureDir(dirname)
}).then(() => {
return new Promise(function(resolve,reject) {
const dirname = fspath.dirname(path);
const tempFile = `${path}.$$$`;
await fs.ensureDir(dirname)
await new Promise(function(resolve,reject) {
var stream = fs.createWriteStream(tempFile);
stream.on('open',function(fd) {
stream.write(content,'utf8',function() {
@@ -110,10 +110,11 @@ module.exports = {
log.warn(log._("storage.localfilesystem.fsync-fail",{path: tempFile, message: err.toString()}));
reject(err);
});
});
}).then(() => {
})
log.trace(`utils.writeFile - written content to ${tempFile}`)
return new Promise(function(resolve,reject) {
await new Promise(function(resolve,reject) {
fs.rename(tempFile,path,err => {
if (err) {
log.warn(log._("storage.localfilesystem.fsync-fail",{path: path, message: err.toString()}));
@@ -122,8 +123,10 @@ module.exports = {
log.trace(`utils.writeFile - renamed ${tempFile} to ${path}`)
resolve();
})
});
});
})
})
writeFileLocks[path] = result.catch(() => {})
return result
},
readFile: readFile,

View File

@@ -14,11 +14,33 @@
* limitations under the License.
**/
var should = require("should");
var NR_TEST_UTILS = require("nr-test-utils");
var util = NR_TEST_UTILS.require("@node-red/runtime/lib/storage/localfilesystem/util");
const should = require("should");
const NR_TEST_UTILS = require("nr-test-utils");
const util = NR_TEST_UTILS.require("@node-red/runtime/lib/storage/localfilesystem/util");
const { mkdtemp, readFile } = require('fs/promises');
const { join } = require('path');
const { tmpdir } = require('os');
describe('storage/localfilesystem/util', function() {
describe('writeFile', function () {
it('manages concurrent calls to modify the same file', async function () {
const testDirectory = await mkdtemp(join(tmpdir(), 'nr-test-'));
const testFile = join(testDirectory, 'foo.txt')
const testBackupFile = testFile + '.$$$'
let counter = 0
const promises = [
util.writeFile(testFile, `update-${counter++}`, testBackupFile ),
util.writeFile(testFile, `update-${counter++}`, testBackupFile ),
util.writeFile(testFile, `update-${counter++}`, testBackupFile )
]
await Promise.all(promises)
const result = await readFile(testFile, { encoding: 'utf-8' })
result.should.equal('update-2')
})
})
describe('parseJSON', function() {
it('returns parsed JSON', function() {
var result = util.parseJSON('{"a":123}');