Remote script execution on Pi


#1

Starting now you can run program and scripts on your Raspberry Pi from the Particle firmware.

To execute a script or command and wait for it to finish do:

Process proc = Process::run("/home/pi/script.sh arg1 arg2");
proc.wait();

The detailed documentation for Process control is here

Here’s an example for publishing the CPU temperature to the cloud:

void loop() {
  // Measure the CPU temperature
  Process proc = Process::run("vcgencmd measure_temp");
  proc.wait();
  // The output is temp=43.5'C
  proc.out().find("=");
  float cpuTemp = proc.out().parseFloat();
  Particle.publish("cpu_temp", String(cpuTemp));
  delay(1000);
}

Feedback on the Process interface is welcome.


Exchanging data with other processes on Raspbery Pi
Linux versus Particle-agent
SD card access (SD/sdFat)
#2

This is awesome.

Edit:
The example above crashes after a little while, and when it does, particle-agent copies tinker over it, deleting the firmware.

terminate called after throwing an instance of 'boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<boost::system::system_error> >'
  what():  resolve: Host not found (authoritative)
Firmware exited with status pid 829 SIGABRT (signal 6)
Quitting firmware gracefully
Entering safe mode because firmware exited too many times in a row. Reverting to Tinker

Update:
I was able to fix this by increasing the delay.


#3

Hi, what delay did you increase? thanks!
Gustavo.


#4

The actual solution was found on this thread:

Basically whenever the Pi’s connection to the particle cloud was broken, the firmware was reset by particle-agent.

This was fixed by the one and only @jvanier

I changed delay(1000); to delay(4000);.


#5

I ran a “whoami” command with this interface, and the processes run as root. This gives an overwhelmingly dangerous level of control to the firmware, and could allow for arbitrary code execution resulting in installation of compromising software if the firmware accepts user input by cloud functions or other mechanisms. This could open up poorly coded firmwares to something akin to the IoT botnets we saw wreaking havoc lately with DynDNS. Are there plans in the future to at least drop the firmware process to a less-privileged user? Running as an unprivileged user may be a hinderance, but it might be a good idea in the name of safety.


#6

The Raspberry Pi GPIO and low-level processes can only be accessed by root, and it because of this why the firmware must run as root.

If your Particle account or access token is compromised then so are all of the devices linked to it. Particle is trying to make their platform as secure as possible to reduce these risks.


#7

Regarding the need for low-level access; that’s fair enough.

However, I can think of many ways this could go awry without having a compromised account / token. The use of webhooks or TCPClient to pull data in from other servers, and then using the retrieved data within system calls / Process::run() could open up the opportunity for a compromised web service (outside of Particle’s control) to execute commands on a Pi.

Granted, the attacker would need to know a lot of specific details to execute such a compromise… across multiple platforms and unique firmwares, but it’s still technically “possible”. Food for thought.


#8

It is very possible. Say you are polling data from somewhere and one day ; rm -rf -no-preserve-root / is attached to the response string and called inside of Process::run(). The firmware would happily go erase your entire SD card. There are even worse commands that could be run and it would just require ; and then the command for them to be executed.


#9

As @nrobinson2000 mentioned, the GPIO access is done by direct memory writes to the GPIO registers which can only be done by root.

The Process::run can run commands as other users by doing Process::run("sudo -u pi my_command"); This would prevent accidentally running commands that affect the system. This can be added to the docs, or this behavior could be the default, unless someone does Process::run_sudo() maybe. What do you think?


Remote execution of a script trigged by Particle cloud event
#10

Would it be better to have two, or maybe three overrides for Process::run()?

The default (as it exists already) could be modified to run as a less privileged user via an implied sudo -u ... (maybe nobody, or even pi? Although some people might remove pi on their device… and nobody would severely limit the amount of functionality available)

Process::run("command");

The second override allows you to specify the name of the user you wish to run as by string:

Process::run("command", "root");

Optionally, the third override allows you to specify the UID of the user you wish to run as by number:

Process::run('command', 0);

NOTE: The problem with using sudo is that it breaks the usual STDIN/STDOUT/STDERR piping semantics. We would need to handle this silently, likely by running sudo -u {{user}} sh -c '{{command}}', so that it doesn’t cause headaches for the users of this functionality. We’d also need to escape single quotes when necessary…


#11

Process::run already runs commands through sh -c {{command}} and the piping is already customized to be able to provide input / capture output. So I don’t expect issues with doing sudo -u {{user}} within Process::run.


#12

I like the overrides you are suggesting, especially:

Process::run("command", "user");

I think this would be good notation for people that are less familiar with bash, and it wouldn’t require them to type sudo -u


#13

Of course, the ability to specify a user manually also opens up the ability to specify a non-existent user; I expect that sudo would probably be the one to break that news unless the firmware checks first. I guess it depends where the error should be handled.


#14

So while we’re talking about security…let’s think about if your particle.io acount was compromised.

All of these talks about overrides for Process::run would be pointless as the attacker would be able to re-flash the firmware. (Not that there isn’t good discussion on the potential for using retrieved information to run in scripts…)

It might be nice to provide some information about how and why it might be a good idea to disable OTA updates with System.disableUpdates() – maybe even with some tutorial on how one could program in failsafes for enabling OTA updates.

I mean it’s all unlikely, but you still generally want to make yourself a harder target than the next guy right? So maybe a tool-tip type thing in the docs/code guide to reflect this would be helpful – don’t want to scare people, but also want them to be informed.


#16

Hello,

This example code by @jvanier for Process::run() is great. It worked very well just a couple of weeks ago (early or mid May 2017) but now has stopped compiling for a Raspberry Pi target.

The example code clip at the very top of this post, copy-pasted generates this compiler error:

processexample.ino:7:3: error: 'Process' was not declared in this scope
   Process proc = Process::run("vcgencmd measure_temp");
   ^
processexample.ino:7:11: error: expected ';' before 'proc'
   Process proc = Process::run("vcgencmd measure_temp");
       ^
processexample.ino:8:3: error: 'proc' was not declared in this scope
   proc.wait();
   
Error: Could not compile. Please review your code.

The compiler error text is formatted nicely in the web IDE but otherwise it appears the Process #include header is no longer on the compiler line.

The complete raw compiler error is here:

Processing processexample.ino make -C ../newlib_nano make[1]: Entering directory '/firmware/newlib_nano' make[1]: Nothing to be done for 'all'. make[1]: Leaving directory '/firmware/newlib_nano' make -C ../user make[1]: Entering directory '/firmware/user' Building cpp file: processexample.cpp Invoking: ARM GCC CPP Compiler mkdir -p ../build/target/user/platform-0-lto arm-none-eabi-gcc -DSTM32_DEVICE -DSTM32F10X_MD -DPLATFORM_THREADING=0 -DPLATFORM_ID=0 -DPLATFORM_NAME=core -DUSBD_VID_SPARK=0x1D50 -DUSBD_PID_DFU=0x607F -DUSBD_PID_CDC=0x607D -DSPARK_PLATFORM -DFLASHEE_EEPROM -g3 -gdwarf-2 -Os -mcpu=cortex-m3 -mthumb -flto -DINCLUDE_PLATFORM=1 -DPRODUCT_ID=0 -DPRODUCT_FIRMWARE_VERSION=65535 -DUSE_STDPERIPH_DRIVER -DDFU_BUILD_ENABLE -DSYSTEM_VERSION_STRING=0.6.2 -DRELEASE_BUILD -I./inc -I../wiring/inc -I../system/inc -I../services/inc -I../communication/src -I../hal/inc -I../hal/shared -I../hal/src/core -I../hal/src/stm32 -I../platform/shared/inc -I../platform/MCU/STM32F1xx/STM32_StdPeriph_Driver/inc -I../platform/MCU/STM32F1xx/STM32_USB_Device_Driver/inc -I../platform/MCU/STM32F1xx/SPARK_Firmware_Driver/inc -I../platform/MCU/shared/STM32/inc -I../platform/MCU/STM32F1xx/CMSIS/Include -I../platform/MCU/STM32F1xx/CMSIS/Device/ST/Include -I../platform/NET/CC3000/CC3000_Host_Driver -I../dynalib/inc -I -I./libraries -I -I -I -I -I. -MD -MP -MF ../build/target/user/platform-0-ltoprocessexample.o.d -ffunction-sections -fdata-sections -Wall -Wno-switch -Wno-error=deprecated-declarations -fmessage-length=0 -fno-strict-aliasing -DSPARK=1 -DPARTICLE=1 -DSTART_DFU_FLASHER_SERIAL_SPEED=14400 -DSTART_YMODEM_FLASHER_SERIAL_SPEED=28800 -DSPARK_PLATFORM_NET=CC3000 -fno-builtin-malloc -fno-builtin-free -fno-builtin-realloc -DLOG_INCLUDE_SOURCE_INFO=1 -DPARTICLE_USER_MODULE -DUSE_THREADING=0 -DUSE_SPI=SPI -DUSE_CS=A2 -DUSE_SPI=SPI -DUSE_CS=A2 -DUSE_THREADING=0 -DMODULE_VERSION=0 -DMODULE_FUNCTION=3 -DMODULE_DEPENDENCY=0,0,0 -D_WINSOCK_H -D_GNU_SOURCE -DLOG_MODULE_CATEGORY="\"app\"" -fno-exceptions -fno-rtti -fcheck-new -std=gnu++11 -c -o ../build/target/user/platform-0-ltoprocessexample.o processexample.cpp processexample.ino: In function 'void loop()': processexample.ino:7:3: error: 'Process' was not declared in this scope Process proc = Process::run("vcgencmd measure_temp"); ^ processexample.ino:7:11: error: expected ';' before 'proc' Process proc = Process::run("vcgencmd measure_temp"); ^ processexample.ino:8:3: error: 'proc' was not declared in this scope proc.wait(); ^ ../build/module.mk:267: recipe for target '../build/target/user/platform-0-ltoprocessexample.o' failed make[1]: Leaving directory '/firmware/user'

Is this example code clip still compiling on your end? Is there an include missing or is there a way to add it?

The web IDE is really cool but provides no way to grep the headers.

Marc


#17

Yes, this still compiles, but your error output does suggest you are not building for a RPi, but for Spark Core

platform-0 stands for Spark Core, RPi would be platform-31

Double check that you have selected Raspberry Pi as target in the Device drawer.


#18

Hello @ScruffR,

Perfect. Your help solved this issue. It compiles just fine once the target device is selected. I recently added another device and had not figured out how to target one versus another.

With no device selected (i.e. no yellow star on the devices pane) the compiler defaults to spark, just as you pointed out.

By clicking the star on the left hand side of a device, it selects that device for compilation and targeting. That selection includes the target-specific libraries during compiling and linking. I didn’t know platform-0 means spark and platform-31 means Raspberry Pi. Good to know.

Image illustrating the selection:

Thanks for the careful consideration and discovering this problem.

Marc


CPU Temperature to Particle Cloud
#19

Hi everyone,

This is my first entry since I’m new to particle.
Hope someone can answer.

I managed to execute commands either using the
Process proc = Process::run(“command”);
proc.wait();
or the simple system(“command”)

This works fine with commands like shutdown now/restart now

Now I’m trying to open a browser window using the execution lines.
Why doesn’t it work with e.g. xdg-open URL ?

Sorry if this is a rather stupid question,
Best regards,
Flo


#20

I think you need to set the DISPLAY variable in bash.

Kind of like this:

You could probably use

Process::run("DISPLAY=:0 xdg-open ...")


#21

Thanks for the quick reply nrobinson!
I tried and it doesn’t work.
Also running python scripts doesn’t show any results.
This might be a matter of my setup:

I run the Google Assistant SDK on my Raspi 3
Then, in IFTTT I setup an applet the uses the GA (with one text ingredient) as trigger and publishes an event to Particle. In Particle the text ingredient determines which if function is to be exexuted back on my RasPi.

 if (strcmp(data,"off")==0) {
    // if GA text ingredient is "off"
    digitalWrite(led,LOW);
    digitalWrite(boardLed,LOW);
  }
  else if (strcmp(data,"on")==0) {
    // if GA text ingredient is "on"
    digitalWrite(led,HIGH);
    digitalWrite(boardLed,HIGH);
  }
  else if (strcmp(data,"open")==0) {
    // if GA text ingredient is "open"
    Process proc = Process::run("DISPLAY=:0 xdg-open http://google.de");
    proc.wait();
  }

Controlling the GPIOs works fine (on/off). “open” is also recognized correctly and IFTTT sends the right “answer”. Just the run() functions only works for shutdown/restart so far.

It’d be awsome to be able to run any script and finally have the unltimate control what GA does for me.
Any ideas?

Best regards,
Flo