Repository: node-red/node-red (js)

Issue:
Uncaught exception when writing .sessions.json
### Current Behavior

We noticed we sometimes get an uncaught exception, it seems related to the .sessions.json file:

```
2023-06-11T10:12:32: 11 Jun 10:12:32 - [warn] Flushing file c:\runtime\service\.sessions.json to disk failed : Error: ENOENT: no such file or directory, rename 'c:\runtime\ service\.sessions.json.$$$' -> 'c:\runtime\ service\.sessions.json'
2023-06-11T10:12:32: 11 Jun 10:12:32 - [red] Uncaught Exception:
2023-06-11T10:12:32: 11 Jun 10:12:32 - [error] Error: ENOENT: no such file or directory, rename 'c:\runtime\wim\.sessions.json.$$$' -> 'c:\runtime\ service\.sessions.json'
2023-06-11T10:12:32: child process exited with code 1
```

### Expected Behavior

We assume this should not usually crash.

### Steps To Reproduce

_No response_

### Example flow

```
[{"id":"49e06aa92c948d58","type":"http request","z":"df623cb7e06fbad6","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":710,"y":480,"wires":[[]]},{"id":"164f2c25a3828c3c","type":"function","z":"df623cb7e06fbad6","name":"getToken","func":"msg.payload = {\n  client_id: 'node-red-admin',\n  grant_type: 'password',\n  scope: 'read',\n  username: 'admin',\n  password: 'password'\n}\nmsg.url = `http://127.0.0.1:1880/auth/token`\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":480,"wires":[["49e06aa92c948d58"]]},{"id":"96c73b3086a6182f","type":"inject","z":"df623cb7e06fbad6","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1,2]","payloadType":"json","x":190,"y":480,"wires":[["9c8f35f58f210b75"]]},{"id":"9c8f35f58f210b75","type":"split","z":"df623cb7e06fbad6","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":350,"y":480,"wires":[["164f2c25a3828c3c"]]}]
```


### Environment

- Node-RED version: 3.0.2
- Node.js version: 18.13
- npm version: -
- Platform/OS: Windows 11
- Browser: -


Pull request description:
Ensure storage/util.writeFile handles concurrent write attempts
Fixes #4221 

This ensures concurrent calls to the `writeFile` utility function are serialised and handled in order. Due to the async nature of the work done, without this it was possible to hit errors (as per original issue) when multiple calls are made to update the same file.

Patch:
diff --git a/README.md b/README.md
index 3401c3726..a888ef166 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,16 @@
 # Node-RED
 
-https://nodered.org
+http://nodered.org
 
 [![Build Status](https://github.com/node-red/node-red/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/node-red/node-red/actions?query=branch%3Amaster)
 
 Low-code programming for event-driven applications.
 
-![Node-RED: Low-code programming for event-driven applications](https://nodered.org/images/node-red-screenshot.png)
+![Node-RED: Low-code programming for event-driven applications](http://nodered.org/images/node-red-screenshot.png)
 
 ## Quick Start
 
-Check out https://nodered.org/docs/getting-started/ for full instructions on getting
+Check out http://nodered.org/docs/getting-started/ for full instructions on getting
 started.
 
 1. `sudo npm install -g --unsafe-perm node-red`
@@ -19,7 +19,7 @@ started.
 
 ## Getting Help
 
-More documentation can be found [here](https://nodered.org/docs).
+More documentation can be found [here](http://nodered.org/docs).
 
 For further help, or general discussion, please use the [Node-RED Forum](https://discourse.nodered.org) or [slack team](https://nodered.org/slack).
 
diff --git a/package.json b/package.json
index 1864904cf..4be1d843d 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
     "name": "node-red",
     "version": "3.1.0-beta.4",
     "description": "Low-code programming for event-driven applications",
-    "homepage": "https://nodered.org",
+    "homepage": "http://nodered.org",
     "license": "Apache-2.0",
     "repository": {
         "type": "git",
diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js
index 353c2effd..89464bd6c 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/red.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/red.js
@@ -731,7 +731,7 @@ var RED = (function() {
         }
         menuOptions.push({id:"menu-item-help",
             label: RED.settings.theme("menu.menu-item-help.label",RED._("menu.label.help")),
-            href: RED.settings.theme("menu.menu-item-help.url","https://nodered.org/docs")
+            href: RED.settings.theme("menu.menu-item-help.url","http://nodered.org/docs")
         });
         menuOptions.push({id:"menu-item-node-red-version", label:"v"+RED.settings.version, onselect: "core:show-about" });
 
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js
index 66c87b1a9..0cbb0e6f1 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js
@@ -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 {
diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.html b/packages/node_modules/@node-red/nodes/core/function/10-function.html
index 446ef918d..4a07cdbfb 100644
--- a/packages/node_modules/@node-red/nodes/core/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/core/function/10-function.html
@@ -366,7 +366,7 @@
             name: {value:"_DEFAULT_"},
             func: {value:"\nreturn msg;"},
             outputs: {value:1},
-            timeout:{value:RED.settings.functionTimeout || 0},
+            timeout:{value:0},
             noerr: {value:0,required:true,
                     validate: function(v, opt) {
                         if (!v) {
diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js
index 7f2250008..662b7fa47 100644
--- a/packages/node_modules/@node-red/nodes/core/function/10-function.js
+++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js
@@ -521,8 +521,7 @@ module.exports = function(RED) {
     RED.nodes.registerType("function",FunctionNode, {
         dynamicModuleList: "libs",
         settings: {
-            functionExternalModules: { value: true, exportable: true },
-            functionTimeout: { value:0, exportable: true }
+            functionExternalModules: { value: true, exportable: true }
         }
     });
     RED.library.register("functions");
diff --git a/packages/node_modules/@node-red/nodes/locales/de/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/de/function/10-function.html
index 4faa03781..55741d643 100644
--- a/packages/node_modules/@node-red/nodes/locales/de/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/locales/de/function/10-function.html
@@ -25,7 +25,7 @@
     <p>Wenn ein promise-Objekt aus dem Start-Code zurückgegeben wird,
        beginnt danach die reguläre Verarbeitung der Eingangsnachrichten.</p>
     <h3>Details</h3>
-    <p>Siehe  <a target="_blank" href="https://nodered.org/docs/writing-functions.html">Onlinedokumentation</a>
+    <p>Siehe  <a target="_blank" href="http://nodered.org/docs/writing-functions.html">Onlinedokumentation</a>
        für weitere Informationen zum Schreiben von Funktionen.</p>
     <h4><b>Nachrichten senden</b></h4>
     <p>Die Funktion kann die Nachrichten zurückgeben, die sie an die nächsten Nodes im Flow weitergeben möchte,
diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/en-US/function/10-function.html
index 99e28e354..831021917 100644
--- a/packages/node_modules/@node-red/nodes/locales/en-US/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/locales/en-US/function/10-function.html
@@ -26,7 +26,7 @@
     <p>If the On Start code returns a Promise object, the node will not start handling messages
         until the promise is resolved.</p>
     <h3>Details</h3>
-    <p>See the <a target="_blank" href="https://nodered.org/docs/writing-functions.html">online documentation</a>
+    <p>See the <a target="_blank" href="http://nodered.org/docs/writing-functions.html">online documentation</a>
     for more information on writing functions.</p>
     <h4>Sending messages</h4>
     <p>The function can either return the messages it wants to pass on to the next nodes
diff --git a/packages/node_modules/@node-red/nodes/locales/fr/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/fr/function/10-function.html
index 11f9a5ac2..8877ce9bb 100644
--- a/packages/node_modules/@node-red/nodes/locales/fr/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/locales/fr/function/10-function.html
@@ -26,7 +26,7 @@
     <p>Si le code 'Au démarrage' renvoie un objet Promise (promesse), le noeud ne commencera pas à gérer les messages
       jusqu'à ce que la promesse soit résolue.</p>
     <h3>Détails</h3>
-    <p>Voir la <a target="_blank" href="https://nodered.org/docs/writing-functions.html">documentation en ligne</a>
+    <p>Voir la <a target="_blank" href="http://nodered.org/docs/writing-functions.html">documentation en ligne</a>
       pour plus d'informations sur les fonctions d'écriture.</p>
     <h4>Envoi de messages</h4>
     <p>La fonction peut envoyer les messages qu'elle souhaite transmettre aux noeuds suivants
diff --git a/packages/node_modules/@node-red/nodes/locales/ja/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/ja/function/10-function.html
index fb6e532f4..960b755f6 100644
--- a/packages/node_modules/@node-red/nodes/locales/ja/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/locales/ja/function/10-function.html
@@ -22,7 +22,7 @@
     <p><b>初期化処理</b>タブにはノードの開始時に実行されるコードを、<b>終了処理</b>タブにはノードの終了時に実行されるコードを指定します。</p>
     <p>初期化処理タブの返却値としてPromiseオブジェクトを返却すると、入力メッセージの処理を開始する前にその完了を待ちます。</p>
     <h3>詳細</h3>
-    <p>コードの書き方の詳細については、<a target="_blank" href="https://nodered.org/docs/writing-functions.html">オンラインドキュメント</a>を参照してください。</p>
+    <p>コードの書き方の詳細については、<a target="_blank" href="http://nodered.org/docs/writing-functions.html">オンラインドキュメント</a>を参照してください。</p>
     <h4>メッセージの送信</h4>
     <p>フロー内の次ノードにメッセージを渡すためには、メッセージを返却するか<code>node.send(messages)</code>を呼び出します。</p>
     <p>返却/sendの対象は次のとおりです:</p>
diff --git a/packages/node_modules/@node-red/nodes/locales/ko/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/ko/function/10-function.html
index 827bb742b..464030fa3 100644
--- a/packages/node_modules/@node-red/nodes/locales/ko/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/locales/ko/function/10-function.html
@@ -20,7 +20,7 @@
     <p><code>msg</code>오브젝트는<code>msg.payload</code>프로퍼티에 메시지 본체를 유지하는 것이 관례입니다.</p>
     <p>보통 코드는 메시지 오브젝트(혹은 여러 메시지 객체)를 반환합니다.후속 플로우의 실행을 정지하고 싶은 경우에는, 오브젝트를 반환하지 않아도 상관없습니다.</p>
     <h3>상세</h3>
-    <p>코드 쓰는 방식에 대한 자세한 내용은, <a target="_blank" href="https://nodered.org/docs/writing-functions.html">공식 홈페이지</a>를 참조해 주세요.</p>
+    <p>코드 쓰는 방식에 대한 자세한 내용은, <a target="_blank" href="http://nodered.org/docs/writing-functions.html">공식 홈페이지</a>를 참조해 주세요.</p>
     <h4>메세지 송신</h4>
     <p>플로우 내의 다음 노드에 메세지를 전달하기 위해서는, 메세지를 반환하거나, <code>node.send(messages)</code>를 호출합니다.</p>
     <p>반환/send 대상은 다음과 같습니다:</p>
diff --git a/packages/node_modules/@node-red/nodes/locales/pt-BR/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/pt-BR/function/10-function.html
index ad272c2c6..9791424aa 100644
--- a/packages/node_modules/@node-red/nodes/locales/pt-BR/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/locales/pt-BR/function/10-function.html
@@ -26,7 +26,7 @@
       <p>Se o código <b>On Start</b> retornar um objeto do tipo promessa, o nó não começará a tratar as mensagens
           até que a promessa seja resolvida.</p>
       <h3>Detalhes</h3>
-      <p>Consulte a <a target="_blank" href="https://nodered.org/docs/writing-functions.html">documentação online</a>
+      <p>Consulte a <a target="_blank" href="http://nodered.org/docs/writing-functions.html">documentação online</a>
       para obter maiores informações sobre funções de escrita.</p>
       <h4>Enviando mensagens</h4>
       <p>A função pode retornar as mensagens que deseja passar para os próximos nós
diff --git a/packages/node_modules/@node-red/nodes/locales/ru/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/ru/function/10-function.html
index b636753d5..9ba16d4be 100644
--- a/packages/node_modules/@node-red/nodes/locales/ru/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/locales/ru/function/10-function.html
@@ -36,7 +36,7 @@
 
   <h3>Подробности</h3>
   <p>
-    Смотрите <a target="_blank" href="https://nodered.org/docs/writing-functions.html">онлайн-документацию</a> для получения дополнительной информации по написанию функций.
+    Смотрите <a target="_blank" href="http://nodered.org/docs/writing-functions.html">онлайн-документацию</a> для получения дополнительной информации по написанию функций.
   </p>
 
   <h4>Отправка сообщений</h4>
diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-function.html
index 861e31f53..035ccc81f 100644
--- a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/10-function.html
@@ -20,7 +20,7 @@
     <p>通常，<code>msg</code>对象将消息正文保留在<code>msg.payload</code>属性中。</p>
     <p>该函数一般会返回一个消息对象（或多个消息对象），但也可以为了停止流而什么都不返回。</p>
     <h3>详细</h3>
-    <p>请参见<a target="_blank" href="https://nodered.org/docs/writing-functions.html">在线文档</a>来获得更多有关编写函数的信息。</p>
+    <p>请参见<a target="_blank" href="http://nodered.org/docs/writing-functions.html">在线文档</a>来获得更多有关编写函数的信息。</p>
     <h4>传送消息</h4>
     <p>要将消息传递到流中的下一个节点，请返回消息或调用<code>node.send(messages)</code>。</p>
     <p>它将返回/send:</p>
diff --git a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-function.html b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-function.html
index c5088d5f0..9f8ddb43f 100644
--- a/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-function.html
+++ b/packages/node_modules/@node-red/nodes/locales/zh-TW/function/10-function.html
@@ -20,7 +20,7 @@
   <p>通常，<code>msg</code>對象將消息正文保留在<code>msg.payload</code>屬性中。</p>
   <p>該函數一般會返回一個消息對象（或多個消息對象），但也可以爲了停止流程而什麽都不返回。</p>
   <h3>詳細</h3>
-  <p>請參見<a target="_blank" href="https://nodered.org/docs/writing-functions.html">在線文檔</a>來獲得更多有關編寫函數的信息。</p>
+  <p>請參見<a target="_blank" href="http://nodered.org/docs/writing-functions.html">在線文檔</a>來獲得更多有關編寫函數的信息。</p>
   <h4>傳送消息</h4>
   <p>要將消息傳遞到流程中的下一個節點，請返回消息或調用<code>node.send(messages)</code>。</p>
   <p>它將返回/send:</p>
diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/util.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/util.js
index b91a99129..e386368eb 100644
--- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/util.js
+++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/util.js
@@ -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,
 
diff --git a/packages/node_modules/node-red/README.md b/packages/node_modules/node-red/README.md
index b6692ca0a..5e149335a 100644
--- a/packages/node_modules/node-red/README.md
+++ b/packages/node_modules/node-red/README.md
@@ -1,14 +1,14 @@
 # Node-RED
 
-https://nodered.org
+http://nodered.org
 
 Low-code programming for event-driven applications.
 
-![Node-RED: Low-code programming for event-driven applications.](https://nodered.org/images/node-red-screenshot.png)
+![Node-RED: Low-code programming for event-driven applications.](http://nodered.org/images/node-red-screenshot.png)
 
 ## Quick Start
 
-Check out https://nodered.org/docs/getting-started/ for full instructions on getting
+Check out http://nodered.org/docs/getting-started/ for full instructions on getting
 started.
 
 1. `sudo npm install -g --unsafe-perm node-red`
@@ -17,7 +17,7 @@ started.
 
 ## Getting Help
 
-More documentation can be found [here](https://nodered.org/docs).
+More documentation can be found [here](http://nodered.org/docs).
 
 For further help, or general discussion, please use the [Node-RED Forum](https://discourse.nodered.org) or [slack team](https://nodered.org/slack).
 
diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json
index d5b3c3868..e4f8eb103 100644
--- a/packages/node_modules/node-red/package.json
+++ b/packages/node_modules/node-red/package.json
@@ -2,7 +2,7 @@
     "name": "node-red",
     "version": "3.1.0-beta.4",
     "description": "Low-code programming for event-driven applications",
-    "homepage": "https://nodered.org",
+    "homepage": "http://nodered.org",
     "license": "Apache-2.0",
     "repository": {
         "type": "git",
diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js
index 63c0188bd..254a9b55b 100755
--- a/packages/node_modules/node-red/red.js
+++ b/packages/node_modules/node-red/red.js
@@ -88,7 +88,7 @@ if (parsedArgs.help) {
     console.log("  -?, --help           show this help");
     console.log("  admin <command>      run an admin command");
     console.log("");
-    console.log("Documentation can be found at https://nodered.org");
+    console.log("Documentation can be found at http://nodered.org");
     process.exit();
 }
 
diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js
index 80b559030..218baa92d 100644
--- a/packages/node_modules/node-red/settings.js
+++ b/packages/node_modules/node-red/settings.js
@@ -71,7 +71,7 @@ module.exports = {
  ******************************************************************************/
 
     /** To password protect the Node-RED editor and admin API, the following
-     * property can be used. See https://nodered.org/docs/security.html for details.
+     * property can be used. See http://nodered.org/docs/security.html for details.
      */
     //adminAuth: {
     //    type: "credentials",
@@ -120,7 +120,7 @@ module.exports = {
      * including node-red-dashboard, or the static content (httpStatic), the
      * following properties can be used.
      * The `pass` field is a bcrypt hash of the password.
-     * See https://nodered.org/docs/security.html#generating-the-password-hash
+     * See http://nodered.org/docs/security.html#generating-the-password-hash
      */
     //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
     //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
@@ -444,7 +444,6 @@ module.exports = {
  *  - fileWorkingDirectory
  *  - functionGlobalContext
  *  - functionExternalModules
- *  - functionTimeout
  *  - nodeMessageBufferMaxLength
  *  - ui (for use with Node-RED Dashboard)
  *  - debugUseColors
@@ -469,9 +468,6 @@ module.exports = {
     /** Allow the Function node to load additional npm modules directly */
     functionExternalModules: true,
 
-    /** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */
-    functionTimeout: 0,
-
     /** The following property can be used to set predefined values in Global Context.
      * This allows extra node modules to be made available with in Function node.
      * For example, the following:


Test patch:
diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js
index acb693208..99557f0b3 100644
--- a/test/nodes/core/function/10-function_spec.js
+++ b/test/nodes/core/function/10-function_spec.js
@@ -1449,34 +1449,6 @@ describe('function node', function() {
         });
     });
 
-    it('check if default function timeout settings are recognized', function (done) {
-        RED.settings.functionTimeout = 0.01;
-        var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}];
-        helper.load(functionNode, flow, function () {
-            var n1 = helper.getNode("n1");
-            n1.receive({ payload: "foo", topic: "bar" });
-            setTimeout(function () {
-                try {
-                    helper.log().called.should.be.true();
-                    var logEvents = helper.log().args.filter(function (evt) {
-                        return evt[0].type == "function";
-                    });
-                    logEvents.should.have.length(1);
-                    var msg = logEvents[0][0];
-                    msg.should.have.property('level', helper.log().ERROR);
-                    msg.should.have.property('id', 'n1');
-                    msg.should.have.property('type', 'function');
-                    should.equal(RED.settings.functionTimeout, 0.01);
-                    should.equal(msg.msg.message, 'Script execution timed out after 10ms');
-                    delete RED.settings.functionTimeout;
-                    done();
-                } catch (err) {
-                    done(err);
-                }
-            }, 500);
-        });
-    });
-
     describe("finalize function", function() {
 
         it('should execute', function(done) {
diff --git a/test/unit/@node-red/editor-api/lib/editor/theme_spec.js b/test/unit/@node-red/editor-api/lib/editor/theme_spec.js
index 2c660415f..fd1b42d61 100644
--- a/test/unit/@node-red/editor-api/lib/editor/theme_spec.js
+++ b/test/unit/@node-red/editor-api/lib/editor/theme_spec.js
@@ -98,7 +98,7 @@ describe("api/editor/theme", function () {
                 },
                 header: {
                     title: "Test Header Title",
-                    url: "https://nodered.org",
+                    url: "http://nodered.org",
                     image: "/absolute/path/to/header/image" // or null to remove image
                 },
 
@@ -147,7 +147,7 @@ describe("api/editor/theme", function () {
         context.page.tabicon.should.have.a.property("colour", "#8f008f")
         context.should.have.a.property("header");
         context.header.should.have.a.property("title", "Test Header Title");
-        context.header.should.have.a.property("url", "https://nodered.org");
+        context.header.should.have.a.property("url", "http://nodered.org");
         context.header.should.have.a.property("image", "theme/header/image");
         context.page.should.have.a.property("css");
         context.page.css.should.have.lengthOf(1);
diff --git a/test/unit/@node-red/runtime/lib/storage/localfilesystem/util_spec.js b/test/unit/@node-red/runtime/lib/storage/localfilesystem/util_spec.js
index fa1e4bdb7..49cccb462 100644
--- a/test/unit/@node-red/runtime/lib/storage/localfilesystem/util_spec.js
+++ b/test/unit/@node-red/runtime/lib/storage/localfilesystem/util_spec.js
@@ -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}');