Very weird happenings with `exec` node and rsync

I am sharing a painful episode I recently had.

Is this a bug? (repeated at the end, but anyway.)

Back story:

I have a NAS which is not always on.
When it is on, I want my 3 Pi's to back up their saved flows to it. (SMB)

Idea:

the NAS would be turned on, a check would see it, back up the files and all is good.

Problem 1:

I had to mount the share to allow things to happen.
SMB is not happy and I had to use fstab to get the share mounted.

Problem 2:

(A lot skipped because they are sort of outside the scope of what I'm sharing here/now)
rsync supports a --dry-run option.
If it is true then it only pretends to do it. If false then it is really done.

eg:
return /usr/bin/rsync ${dryRunOption}-avh --no-o --no-g "${d}" "${remoteBase}/"``
(Line 38 in the first function node)

I have a function node to build the array of commands and that is passed to another function node to split the array up and pass the commands to the second exec node.
(the first one built the arrays)

So - with GPT's help - the code was written and I would change a line in the function node to change the dry-run from true to false so things really worked.

They didn't.
I can't show you old code and didn't know the problem/s at the time.
Just when I found it I realised the problem.

exert from flow.

[
    {
        "id": "6093eee6e177df21",
        "type": "function",
        "z": "7e66ea1ffcc6c504",
        "g": "ac2230dcc7d96f62",
        "name": "Prepare rsync commands",
        "func": "// Check for dryrun toggle message\nif (msg.topic === 'dryrun') {\n    let dryRun = (msg.payload === true || msg.payload === 'true')\n    context.set('dryRun', dryRun)\n    node.status({ text: \"DryRun set to \" + dryRun })\n    return null  // exit early\n}\n\n// Normal processing of directory messages\n// Retrieve dryRun from node context\nlet dryRun = context.get('dryRun') // default undefined = simulate if not set\n\nnode.status({ text: \"DryRun \" + dryRun })\n\n// Split the output of 'find' into individual directories\nlet dirs = msg.payload.split('\\n').filter(s => s.trim() !== '')\n\n// Only include directories after this cutoff (YYYY-MM)\nconst cutoff = '2025-10'\n\n// Filter directories that are after the cutoff\nlet newDirs = dirs.filter(fullPath => {\n    let dirName = fullPath.trim().split('/').pop()  // get last part of path\n    let yearMonth = dirName.substring(0, 7)\n    return yearMonth >= cutoff\n})\n\n// Get the device name from global context\nconst myDevice = global.get(\"myDeviceName\")\n\n// Build rsync commands, including year folder dynamically\nmsg.rsynccmds = newDirs.map(d => {\n    const dryRunOption = dryRun ? '--dry-run ' : ''\n    const dirName = d.trim().split('/').pop()      // e.g., \"2025-11-21.21.51.11\"\n    const year = dirName.substring(0, 4)          // \"2025\"\n    const remoteBase = `/mnt/smb/NR/${myDevice}/${year}`\n\n    return `/usr/bin/rsync ${dryRunOption}-avh --no-o --no-g \"${d}\" \"${remoteBase}/\"`\n})\n\nmsg.index = 0\nreturn msg\n",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 860,
        "y": 920,
        "wires": [
            [
                "e4d287f87bc55feb",
                "59a0fd7f43066660"
            ]
        ]
    },
    {
        "id": "e4d287f87bc55feb",
        "type": "function",
        "z": "7e66ea1ffcc6c504",
        "g": "ac2230dcc7d96f62",
        "name": "Run next rsync",
        "func": "//  Needed to indicate start/stop of sequence\nlet msg1 = {}\n// Stop if no more commands\nif (!msg.rsynccmds || msg.index >= msg.rsynccmds.length) {\n    msg1.payload = 0\n    return [null,msg1]\n}\n\n// Send current command to Exec node\nmsg.payload = msg.rsynccmds[msg.index]\nmsg1.payload = 1\nreturn [msg,msg1]",
        "outputs": 2,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 630,
        "y": 990,
        "wires": [
            [
                "bc71410642c07a9a",
                "16ad656e181e0b97"
            ],
            [
                "6cd51b56501cfa7c"
            ]
        ],
        "outputLabels": [
            "",
            "start/stop for LED"
        ]
    },
    {
        "id": "bc71410642c07a9a",
        "type": "exec",
        "z": "7e66ea1ffcc6c504",
        "g": "ac2230dcc7d96f62",
        "command": "",
        "addpay": "payload",
        "append": "",
        "useSpawn": "false",
        "timer": "",
        "winHide": false,
        "oldrc": false,
        "name": "Run rsync",
        "x": 810,
        "y": 990,
        "wires": [
            [
                "4974f7fe81962fe5",
                "d3affa8f94a261ef"
            ],
            [],
            []
        ]
    },
    {
        "id": "d3affa8f94a261ef",
        "type": "function",
        "z": "7e66ea1ffcc6c504",
        "g": "ac2230dcc7d96f62",
        "name": "Next rsync",
        "func": "msg.index++\nreturn msg",
        "outputs": 1,
        "x": 980,
        "y": 1010,
        "wires": [
            [
                "e4d287f87bc55feb"
            ]
        ]
    },
    {
        "id": "f265fde7c6335fc7",
        "type": "inject",
        "z": "7e66ea1ffcc6c504",
        "name": "Simulate",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "dryrun",
        "payload": "true",
        "payloadType": "bool",
        "x": 600,
        "y": 650,
        "wires": [
            [
                "6093eee6e177df21"
            ]
        ]
    },
    {
        "id": "32453af4e7ef9c48",
        "type": "inject",
        "z": "7e66ea1ffcc6c504",
        "name": "Do",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "dryrun",
        "payload": "false",
        "payloadType": "bool",
        "x": 590,
        "y": 690,
        "wires": [
            [
                "6093eee6e177df21"
            ]
        ]
    }
]

So I would set/inject the DO trigger the flow and nothing would happen. TIME AND TIME AGAIN.

The entire command was sent as the payload.
eg:

Copied the command and did it FOR REAL and it would work.

WTF is going on?
/usr/bin/rsync -avh --no-o --no-g "/home/pi/Public/NR/2025-11-21.21.51.11" "/mnt/smb/NR/TimePi/2025/"

NO MENTION of --dry-run anywhere.

As a test, I put that command into a new exec node and tried.
It worked.
So this established that it could work.
What's going on?

Opened the second exec node. (from flow posted) (Force rsync) and saw this:
(simulated now)

Removed the true and left it blank.

ALL WORKING

Errr..... Bug?
I don't understand how it would force rsync to do a dry-run if that was set to true.
I can't see the link/association.

Some one?

// Check for dryrun toggle message
if (msg.topic === 'dryrun') {
    let dryRun = (msg.payload === true || msg.payload === 'true')
    context.set('dryRun', dryRun)
    node.status({ text: "DryRun set to " + dryRun })
    return null  // exit early
}

When you topic is dryrun you exit the function node with null. The rest of the function node code never gets run.

(Thanks. At least you replied to this.)

This is true for the code now in the function node.

It wasn't always that code. Sorry the older code is lost.

But just wanting to check we are both on the same page - as it were.

That bit of the code is so I can inject/change the dry-run from true to false. and I can test things.

BUT if I have the true in the exec node's extra command parameters field, dry-run is/was always set to true.

How does the true from there change how the command is parsed?

SHOULD IT - change that part of the command?

I am not sure that the exec node had anything to do with it. Check that context dryrun is set to undefined when running for real.

Thanks again.

There is no blame here. This is a story of what happened.

I wanted to rsync stuff and was not getting anywhere at all.

ChatGPT helped me with most of the code and a dry-run option was introduced way back.
It was a hard coded line and to change it I would have to edit the code and set it to false.

Then it became a variable so changing it would be easier as in all I would do is change the variable's setting from true to false rather than editing the actual command being sent.

Through out all this there were problems with permissions - but that's a whole other story.
And so the commands were failing.

Every now and then "we" (Chat GPT and I) would ground things by me actually trying the command in a CLI and seeing what happened.

Eventually the command worked so "we" built on that.
Details here get sketchy to what exactly happened when, so please indulge me a bit here.

So I had the flow and it worked in the greater scheme of things in that things happened when they should in the right order.

I then wanted to change the code in that node so I could toggle the dry-run option without editing it. Each edit was about 40 seconds to redeploy and it was getting tedious waiting between test deploys.

The msg.topic == "dryrun" part was added.

I set it to false. The node echoed this happily and I sent the command. No sorry, that was added way later after I noticed it wasn't working.

Things seemed to work. No errors, nothing saying t failed.
Yes, I maybe should have studied the debug outputs a bit more. But that's water under the bridge now.

It said it was done. Yet when I looked at the destination directory: nada. nothing. zilch.

So I added the mode.status lines.

Then I noticed I would inject the false message, the node would say dry run set to false, but as soon as I sent the REAL message, it would change back to true.

I hadn't opened the exec node as it was only parsing the msg.payload so I didn't think I needed any attention.

So "we" were back to square one: things weren't working.

I tried from the CLI, it worked. Kind of threw a spanner in theories, but also helped/proved we weren't chasing shadows and it was possible.

Given that "we" tested the code again and again. Still "failing".
As in: I would set it to false and as soon as I sent in the real message, it would go back to true.

Again I tested it from the CLI. It worked.

It was agreed that there is something else going on. NR permissions? Who knows?

So to test the last piece I got a new exec node and pasted the command into it and stuck an inject node to it. Deployed and injected.

IT WORKED!!

That is weird.

I opened the existing exec node and noticed the true there. It isn't in the new one.

I deleted it. Deployed and tested.

It worked.

So again, this threw me.

I put it back. It failed. (As in it did a dry-run.)

The part I deleted is ONLY true.

NOT: dry-run == true or anything like that.

So I am stuck understanding how/why the true set in/on the exec node modified the command and made it do a dry-run.

Is my end broken? I can't test that as I only have what I have.
To me it is a bug as I can't see how the association happens/d.

Could someone explain to me the workings on how the true influences the command and confirm that is what is supposed to happen.
(Again, not blame to you.)

Just this is where I'm at and it is confusing as ...... to me what's going on.

Thanks again.

So it's difficult to test anything with the flow you provided as it doesn't produce any output from either of the inject nodes as @Buckskin pointed out.

Also we do not have your folder structure and remote drive.

How exactly are you determining that it is doing a dry run ?

If you put debug nodes at each stage and on all the exec outputs, you will likely see the problem.

I suspect that appending true to the end of your command simply causes the command to fail as it is not a valid parameter.

Ok: yes, you don't have those paths.

But does anyone have/use rsync?

Set up a local thing on your machine and tell me what you see if that field has true in it.

WRT

Does it really need to?

I was showing you the flow.

The outputs - which I am now kind of sorry I didn't capture - didn't indicate any problems.

I copied one of the commands from the rsync output Command debug node and pasted it into the newer exec node and it worked.

So I am asking for someone to double check if my system is broken (implied) by if they user rsync to add the true in that field and confirm what happens on their/your machine.

If my end is broken then I can do all the tests on Earth, and they would mean nothing as I am using a faulty base-line.

I can't see what happens in the exec node and all the outputs looked ok and there was no red dot and error xxx below it.

I looked in the remote directory after the command was injected and nothing was there.
After I deleted the true and ran it again: Files were there.

Hi Andrew
I ran your rsync mini flow and the only time I get --dry-run is when the context variable is set to true. All that putting true in the exec node does is add true to the end of the command;

/usr/bin/rsync -avh --no-o --no-g "2025-10-12" "/mnt/smb/NR/undefined/2025/" true

Note: the undefined is because I do not have your file structure.

In your function node unless you send a msg with payload = false & topic = 'dryrun' prior to running the actual input (which was not included) you will get --dry-run added to the command. Adding true in the Exec node makes no difference to my runs.

1 Like

Thank you thank you thank you.

So that means I somehow missed seeing the true added to the end of the command in my flow.

For my benefit: how did you get to see the command done by the exec node?

All I got o see - from memory was the result of the command on one of the outputs.

All I did was add a debug node connected to all 3 exec outputs. The rc property contains the return message; Note that this only works if the command fails because if it works rc just returns {code: 0}

Command failed:  /usr/bin/rsync -avh --no-o --no-g "2025-10-12" "/mnt/smb/NR/undefined/2025/" true
rsync: [sender] link_stat "/home/pi/2025-10-12" failed: No such file or directory (2)
rsync: [sender] change_dir "/mnt/smb/NR/undefined/2025" failed: No such file or directory (2)
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1338) [sender=3.4.1]
1 Like

Again, appreciated.

I either forgot or overlooked that trick.

Was it not visible if you put a debug node on the output of the function node feeding the exec node?

The word overlooked was included.

I'm not sure exactly what happened.

Now I know.

Oh, I just realised.

But that wouldn't reflect what/how the true in the exec node would do inside the exec node itself.

What do you mean, what it would do? It wouldn't do anything. The exec node is passed a string which it passes to the OS to execute. The node won't even notice if the string ends with the text 'true'.

Can you explain in simple terms what the code snippet is supposed to do?

What message should the prepare node send? I get nothing in the debug.

What are the multiple command lines you are sending to exec?

What are the two different outputs from Run next rsync supposed to be?

This may help. I have translated what Andrew was trying to achieve. Select Activate then Run and see the rc property. Repeat run after Cancel

[{"id":"6093eee6e177df21","type":"function","z":"53a4dcf62732d6d9","name":"Prepare rsync commands","func":"// Check for dryrun toggle message\n\nif (msg.topic === 'dryrun') {\n    let dryRun = (msg.payload === true || msg.payload === 'true')\n    context.set('dryRun', dryRun)\n    node.status({ text: \"DryRun set to \" + dryRun })\n    return null  // exit early\n\n}\n\n// Normal processing of directory messages\n// Retrieve dryRun from node context\nlet dryRun = context.get('dryRun') // default undefined = simulate if not set\n\nnode.status({ text: \"DryRun \" + dryRun })\n\n// Split the output of 'find' into individual directories\nlet dirs = msg.payload.split('\\n').filter(s => s.trim() !== '')\n\n// Only include directories after this cutoff (YYYY-MM)\nconst cutoff = '2025-10'\n\n// Filter directories that are after the cutoff\nlet newDirs = dirs.filter(fullPath => {\n    let dirName = fullPath.trim().split('/').pop()  // get last part of path\n    let yearMonth = dirName.substring(0, 7)\n    return yearMonth >= cutoff\n})\n\n// Get the device name from global context\nconst myDevice = global.get(\"myDeviceName\")\n\n// Build rsync commands, including year folder dynamically\nmsg.rsynccmds = newDirs.map(d => {\n    const dryRunOption = dryRun ? '--dry-run ' : ''\n    const dirName = d.trim().split('/').pop()      // e.g., \"2025-11-21.21.51.11\"\n    const year = dirName.substring(0, 4)          // \"2025\"\n    const remoteBase = `/mnt/smb/NR/${myDevice}/${year}`\n\n    return `/usr/bin/rsync ${dryRunOption}-avh --no-o --no-g \"${d}\" \"${remoteBase}/\"`\n})\n\nmsg.index = 0\nreturn msg\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":4580,"wires":[["e4d287f87bc55feb","5dc8338de306eea7"]]},{"id":"e4d287f87bc55feb","type":"function","z":"53a4dcf62732d6d9","name":"Run next rsync","func":"//  Needed to indicate start/stop of sequence\nlet msg1 = {}\n// Stop if no more commands\nif (!msg.rsynccmds || msg.index >= msg.rsynccmds.length) {\n    msg1.payload = 0\n    return [null,msg1]\n}\n\n// Send current command to Exec node\nmsg.payload = msg.rsynccmds[msg.index]\nmsg1.payload = 1\nreturn [msg,msg1]","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":4690,"wires":[["bc71410642c07a9a"],[]],"outputLabels":["","start/stop for LED"]},{"id":"bc71410642c07a9a","type":"exec","z":"53a4dcf62732d6d9","command":"","addpay":"payload","append":"true","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"Run rsync","x":500,"y":4690,"wires":[["d3affa8f94a261ef","d612ca75b673b25c"],["d612ca75b673b25c"],["d612ca75b673b25c"]]},{"id":"d3affa8f94a261ef","type":"function","z":"53a4dcf62732d6d9","name":"Next rsync","func":"msg.index++\nreturn msg","outputs":1,"x":690,"y":4640,"wires":[["e4d287f87bc55feb"]]},{"id":"f265fde7c6335fc7","type":"inject","z":"53a4dcf62732d6d9","name":"Activate Dryrun","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"dryrun","payload":"true","payloadType":"bool","x":220,"y":4460,"wires":[["6093eee6e177df21"]]},{"id":"32453af4e7ef9c48","type":"inject","z":"53a4dcf62732d6d9","name":"Cancel Dryrun","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"dryrun","payload":"false","payloadType":"bool","x":210,"y":4520,"wires":[["6093eee6e177df21"]]},{"id":"5dc8338de306eea7","type":"debug","z":"53a4dcf62732d6d9","name":"rsync Command","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":900,"y":4560,"wires":[]},{"id":"a6babb0de69c8625","type":"inject","z":"53a4dcf62732d6d9","name":"Run After Activate or Cancel","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"2025-10-12","payloadType":"str","x":260,"y":4580,"wires":[["6093eee6e177df21"]]},{"id":"d612ca75b673b25c","type":"debug","z":"53a4dcf62732d6d9","name":"EXec Out","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":880,"y":4680,"wires":[]}]