coreybutler/node-windows

unable to launch an executable via child process inside a service

wkentdag opened this issue ยท 20 comments

Hi! I'm running into an issue with a Service-wrapped node script that, among other things, needs to launch something like C:\Program Files (x86)\app\app.exe via a child process. Are there any known limitations for the type of thing I'm trying to achieve? I ask because I've spent a bit of time digging into this issue and feel like I'm seeing some buggy behavior. More context:

// app.js (wrapped into a service using its absolute path)
const appExecutable = path.join('C:', 'Program Files (x86)', 'app', 'app.exe')
const launch = path.join(__dirname, 'launch.bat')

// I've ran each of these via `node app.js` (normal) and after building as a service (service)
execa('echo', ['unicorns'])  // logs to `daemon\app.out.log` in normal and service
execa(appExecutable + path.join('some','nonexistent','file.exe')) // logs error to `daemon\app.err.log` in both
execa(appExecutable) // normal: launches app; service: never launches, or returns result or error
execa(launch) // normal: launches app; service: executes script but doesn't start app
.then(console.log)
.catch(console.error)
:: launch.bat

set LOG=C:\Users\will\code\secure-key-fingerprint\host\log.txt
time /t >> %LOG%
"C:\Program Files (x86)\app\app.exe"
echo done >> %LOG%

I've executed the request with both execa and child_process, so I'm fairly confident the issue isn't specific to one of those modules. I've also reinstalled node-windows per the suggestions on the README and swapped out any paths with absolute ones and had no difference in the effect.

windows 10
node 6.6.0
npm 3.10.3

There are a few potential limitations. First, you're referencing __dirname in the launch variable. This would require the launch.bat file to be relative to the wrapper daemon. For testing purposes, I would make sure that's an absolute path.

The C:\Program Files and C:\Program Files (x86) both require elevated administrative permissions to work with. You need to make sure you're passing the right user to the child process. By default, node-windows requires a user with administrative permissions, but that doesn't mean it will spawn/fork a process using the same account.

Hope that helps.

@coreybutler - I created a simple test project eliminating the use of __dirname and launch.bat with the same results. ๐Ÿ˜•

Re: administrative privileges, I'm not normally a windows user, so this could be one point of failure. I have been running these scripts in an elevated Administrator Command Prompt. Are there other steps I can/should take to ensure that I'm spawning child processes with elevated permissions as well? Alternatively, is there another error log (besides daemon\**.log - nothing there) I can check to verify that this is the problem?

Here's a link to my test project, which features the most simple example of my issue: https://github.com/wkentdag/service-launch-test

Hi @wkentdag,
do you have "Allow service to interact with desktop" enabled in the services.msc?
image

Is this electron app a gui taskbar item that you want to launch at startup? if so I'd recommend just adding it to the startup items and avoid services all together.

EDIT

you may want to consider adding
<interactive />
to the xml file generated by node-windows as well.

@jdziat thanks for the suggestion. Unfortunately I'm experiencing the same result after turning on the desktop interaction option and adding the <interactive /> tag as the last XML node before the closing </service> tag.

At first I thought this was a problem with the way I'm trying to use execa, but I'm experiencing the same results when I use child_process, so I think it's something to do with the way this module handles scripts that runs commands that require elevated permission.

The Electron app is a full-fledged desktop .app or .exe, not a taskbar item. I've condensed my example down for the sake of this issue - what we're actually trying to do is have the windows service listen for a specific USB device to get plugged in, and then open the electron app once the device is mounted. So far I've been able to get the service to listen for the USB device and fire off an arbitrary shell command like echo hello, but it either silently fails or ignores my attempts to have it launch an application, whether that's my electron app, Google Chrome, etc

Edit: the more that I think about it the more I'm starting to think that for this use case it would make more sense to bundle everything up in an electron taksbar item, since they already have a predefined process for loading those on startup. But I would still like to figure out a resolution to this issue, as I think it might be a bug, and I'd rather solve this than have to start from scratch

@wkentdag - I didn't realize this was cooperating with an Electron app. I would absolutely use Electron instead of node-windows for this use case. Otherwise, two instances of Node are required on the desktop (native install plus the version built into Electron)... it's redundant in a bad way. I do not think this is a bug with node-windows, as there have been many users who have been able to successfully launch a child process from node-windows.

node-windows wasn't designed to handle elevated permissions for child processes. It's left entirely up to the developer. The best way to accomplish this is to either pass the context/user to the child_process or utilize the windows shell to execute the app in a windows shell (there is a script in the bin directory that could be used for this purpose). In my own work, I just stick to Electron for this type of stuff.

The C:\Program Files and C:\Program Files (x86) both require elevated administrative permissions to work with. You need to make sure you're passing the right user to the child process. By default, node-windows requires a user with administrative permissions, but that doesn't mean it will spawn/fork a process using the same account.

@coreybutler - how can I be sure i'm passing the correct user to the child process? if I'm the only user on my system, whatother account could it be using, and why would it not just default to the account that spawned the original process?

It's not necessarily passing a user to the child user. It's going to be executed by whichever user launched the thread. So it could be a local system account as well. Something you will want to check and account for. Look at the process environment variables or validate it when you spin up the thread.

It's not necessarily passing a user to the child user. It's going to be executed by whichever user launched the thread.

@jdziat I'm a little confused, these two sentences seem to contradict each other. My system user will executes the command node service.js to create the service (I'm assuming this is what happens when I log in to my machine as will and run node service.js within an elevated command prompt). "It's going to be executed by whichever user launched the thread" implies to me that will will also launch the child process within the service. Furthermore, according to the docs,

[...]if you want the service module to run commands as a specific user[...] By default, it will run using the user account that launched the process (i.e. who launched node app.js).

Given that passage in the documentation and the feedback I've received here, it sounds like the service should be executing the child_process with the user will, no? And if the command I'm trying to execute fails for a lack of privileges, shouldn't it log an error somewhere instead of just silently failing?

In any event, the user account seemed like a good bet for the source of the error, and you're right: when I run node app.js (starting my script manually instead of creating a service to execute it), process.env.USERNAME is will, and everything works fine. When I do the same within the service, it's DESKTOP-6R1V5UK$, and it fails silently. sudo seemed like a good bet to execute the child process with will, since "by supplying this, the service module will attempt to run commands using the user account that launched the process and the password for that account." Unfortunately, when running node service.js after adding service.sudo.password = 'mypassword', I get a big ugly error:
sudo-not-working

I also tried setting the user attribute, even though this seems to be geared towards authenticating within a network. I tried the following, with and without the first line:

service.user.domain = 'DESKTOP-6R1V5UK' // pulled from `process.env.USERDOMAIN` when `process.env.USERNAME === will`
service.user.account = 'will'
service.user.password = 'mypassword'

The service still failed silently.

TL;DR

  • the documentation is a bit confusing, as it implies that child processes will be spawned by default with the same user who created the service, which is not the case (happy to PR this fix once I can solve my issue)
  • When I run my script as a service, the user switches from my user will to the system user. As detailed above, both methods suggested in the docs to specify a certain user for the child processes do not appear to work.
  • As has been suggested here, I could use electron to do this, but I think the inability to spawn a child process with a specific user is an issue that I should be able to solve here...plus a windows service is the optimal solution for my use case (don't need the overhead of an electron app).

So when I said launches the thread I mean when the service actually launches. When ran from cod it will be will when it is started as a service it defaults to the local system account. You would have to switch over the service user to the desired user. It can be done programmatically but I do not believe that it is supported from this package.

via Newton Mail [https://cloudmagic.com/k/d/mailapp?ct=pi&cv=9.2.5&pv=10.0.2&source=email_footer_2]
On Tue, Nov 8, 2016 at 5:15 PM, will kent-daggett notifications@github.com wrote:
It's not necessarily passing a user to the child user. It's going to be executed by whichever user launched the thread.

@jdziat [https://github.com/jdziat] I'm a little confused, these two sentences seem to contradict each other. My system user will executes the command node service.js to create the service (I'm assuming this is what happens when I log in to my machine as will and run node service.js within an elevated command prompt). "It's going to be executed by whichever user launched the thread" implies to me that will will also launch the child process within the service. Furthermore, according to the docs [https://github.com/coreybutler/node-windows#user-account-attributes] ,

[...]if you want the service module to run commands as a specific user[...] By default, it will run using the user account that launched the process (i.e. who launched node app.js).

Given that passage in the documentation and the feedback I've received here, it sounds like the service should be executing the child_process with the user will , no? And if the command I'm trying to execute fails for a lack of privileges, shouldn't it log an error somewhere instead of just silently failing?

In any event, the user account seemed like a good bet for the source of the error, and you're right: when I run node app.js (starting my script manually instead of creating a service to execute it), process.env.USERNAME is will , and everything works fine. When I do the same within the service, it's DESKTOP-6R1V5UK$ , and it fails silently. sudo seemed like a good bet to execute the child process with will , since "by supplying this, the service module will attempt to run commands using the user account that launched the process and the password for that account." Unfortunately, when running node service.js after adding service.sudo.password = 'mypassword' , I get a big ugly error:
[https://cloud.githubusercontent.com/assets/3254957/20121140/bed1dcd6-a5c4-11e6-9ac0-0f56c4f70bc8.PNG]

I also tried setting the user attribute, even though this seems to be geared towards authenticating within a network. I tried the following, with and without the first line:

service.user.domain='DESKTOP-6R1V5UK'// pulled from process.env.USERDOMAIN when process.env.USERNAME === willservice.user.account='will'service.user.password='mypassword'

The service still failed silently.

TL;DR

  • the documentation is a bit confusing, as it implies that child processes will be spawned by default with the same user who created the service, which is not the case (happy to PR this fix once I can solve my issue)
  • When I run my script as a service, the user switches from my user will to the system user. As detailed above, both methods suggested in the docs to specify a certain user for the child processes do not appear to work.
  • As has been suggested here, I could use electron to do this, but I think the inability to spawn a child process with a specific user is an issue that I should be able to solve here...plus a windows service is the optimal solution for my use case (don't need the overhead of an electron app).

โ€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub [https://github.com//issues/138#issuecomment-259289800] , or mute the thread [https://github.com/notifications/unsubscribe-auth/ABITyhH-BmIclWMnsihZ5uUAI6F9o-RWks5q8QJ1gaJpZM4Ki7US] .

You would have to switch over the service user to the desired user. It can be done programmatically but I do not believe that it is supported from this package.

If this is correct, it directly conflicts with the documentation):

By supplying [sudo.password], the service module will attempt to run commands using the user account that launched the process and the password for that account.

I verified that my system user spawns the child process when the script is launched with a windows service. When I attempt to force it to launch with my administrative user account, sudo.exe crashes. What am I missing?

I think we're confusing the contexts of execution. I don't use sudo in my implementation though. Have you looked through the source yet? You can submit a PR and update the documentation. I still think that this falls outside of the scope of what this module is intended to do though.

via Newton Mail [https://cloudmagic.com/k/d/mailapp?ct=pi&cv=9.2.5&pv=10.0.2&source=email_footer_2]
On Wed, Nov 9, 2016 at 2:53 PM, will kent-daggett notifications@github.com wrote:
You would have to switch over the service user to the desired user. It can be done programmatically but I do not believe that it is supported from this package.

If this is correct, it directly conflicts with the documentation):

By supplying [ sudo.password ], the service module will attempt to run commands using the user account that launched the process and the password for that account.

I verified that my system user spawns the child process when the script is launched with a windows service. When I attempt to force it to launch with my administrative user account, sudo.exe crashes. What am I missing?

โ€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub [https://github.com//issues/138#issuecomment-259523565] , or mute the thread [https://github.com/notifications/unsubscribe-auth/ABITyg1p4q2zbIs0hwGgxH4M5wNrNBdVks5q8jLCgaJpZM4Ki7US] .

I am happy to submit a PR but I still feel like I'm missing something.

@coreybutler and @jdziat you have both suggested here that it is indeed possible to spawn a child process with administrative privileges. The README section that I have linked to clearly indicates that sudo is one method you can use to accomplish this. So does the source code. When I try to use sudo to elevate my administrative privileges, the service crashes on install. Either I am using this feature incorrectly or it has a bug. How does this use case fall outside of the scope of the module? I'm trying to do something fairly simple using boilerplate code lifted from the module's own documentation.

Again, I will happily submit a PR to help alleviate the issue - ideally that would mean replacing/appending the current sudo example. However, until I can get it to work, the only PR I would make would be to just remove sudo from the readme and the source code because it doesn't appear to be working as intended!

So if it fails on install the user executing the code has insufficient privileges (at least usually).

via Newton Mail [https://cloudmagic.com/k/d/mailapp?ct=pi&cv=9.2.5&pv=10.0.2&source=email_footer_2]
On Wed, Nov 9, 2016 at 3:41 PM, will kent-daggett notifications@github.com wrote:
I am happy to submit a PR but I still feel like I'm missing something.

@coreybutler [https://github.com/coreybutler] and @jdziat [https://github.com/jdziat] you have both suggested here that it is indeed possible to spawn a child process with administrative privileges. The README section that I have linked to clearly indicates that sudo is one method you can use to accomplish this. So does the source code [https://github.com/coreybutler/node-windows/blob/master/lib/binaries.js#L60] . When I try to use sudo to elevate my administrative privileges, the service crashes on install. Either I am using this feature incorrectly or it has a bug. How does this use case fall outside of the scope of the module? I'm trying to do something fairly simple using boilerplate code lifted from the module's own documentation.

Again, I will happily submit a PR to help alleviate the issue - ideally that would mean replacing/appending the current sudo example. However, until I can get it to work, the only PR I would make would be to just remove sudo from the readme and the source code because it doesn't appear to be working as intended!

โ€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub [https://github.com//issues/138#issuecomment-259534593] , or mute the thread [https://github.com/notifications/unsubscribe-auth/ABITyrpSN49GoL2cEkpw7OUIeKHx1FtYks5q8j4QgaJpZM4Ki7US] .

@jdziat I don't think that's the source of my issue. Again, I am the only user on my system, I have administrative privileges, and I'm running all of these commands within an eleveated prompt...and the script runs fine when I bypass the service. I don't usually use windows so I guess I could be overlooking something here but really don't think so.

The service uses a different context then the prompt. Check services.msc

via Newton Mail [https://cloudmagic.com/k/d/mailapp?ct=pi&cv=9.2.5&pv=10.0.2&source=email_footer_2]
On Wed, Nov 9, 2016 at 4:20 PM, will kent-daggett notifications@github.com wrote:
@jdziat [https://github.com/jdziat] I don't think that's the source of my issue. Again, I am the only user on my system, I have administrative privileges, and I'm running all of these commands within an eleveated prompt...and the script runs fine when I bypass the service. I don't usually use windows so I guess I could be overlooking something here but really don't think so.

โ€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub [https://github.com//issues/138#issuecomment-259543531] , or mute the thread [https://github.com/notifications/unsubscribe-auth/ABITykJ3813qPJs9lubuwg2C5R2xN64Fks5q8kc1gaJpZM4Ki7US] .

Again...I am the only user on my system. Therefore, if it's being executed by a different user, it must be the system user. Which brings me back to my original question: how do I tell the service to spawn a child process with my user? sudo should work, but it doesn't.

So think of services as daemons. The script you're executing is the install script. The context of the script when it is ran is the user assigned to execute the daemon

via Newton Mail [https://cloudmagic.com/k/d/mailapp?ct=pi&cv=9.2.5&pv=10.0.2&source=email_footer_2]
On Wed, Nov 9, 2016 at 4:41 PM, will kent-daggett notifications@github.com wrote:
Again...I am the only user on my system. Therefore, if it's being executed by a different user, it must be the system user. Which brings me back to my original question: how do I tell the service to spawn a child process with my user? sudo should work, but it doesn't.

โ€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub [https://github.com//issues/138#issuecomment-259547907] , or mute the thread [https://github.com/notifications/unsubscribe-auth/ABITytakhtq4b_Ati8ptyGsCMwvuWzIOks5q8kwtgaJpZM4Ki7US] .

Right. I understand that. The question is, how can I assign the user to execute the daemon, and why isn't sudo accomplishing this?

Because sudo has its own set of limitations. What's your actual use case? I'll do my best to steer you in the right direction

via Newton Mail [https://cloudmagic.com/k/d/mailapp?ct=pi&cv=9.2.5&pv=10.0.2&source=email_footer_2]
On Wed, Nov 9, 2016 at 5:39 PM, will kent-daggett notifications@github.com wrote:
Right. I understand that. The question is, how can I assign the user to execute the daemon, and why isn't sudo accomplishing this?

โ€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub [https://github.com//issues/138#issuecomment-259558905] , or mute the thread [https://github.com/notifications/unsubscribe-auth/ABITyoCEa661odvPI9KEZ_R5BKPo7XfCks5q8lmmgaJpZM4Ki7US] .

Sorry I missed most of this chain. This conversation was happening while I was prepping for Node Interactive and I never saw the notifications. @wkentdag has most likely worked through/around this by now, but I wanted to clarify a few things.

sudo

The sudo feature relies on a 3rd party binary based on sudowin. I've thought about stripping it out of this library since there isn't much I can do about issues with it, other than refer them upstream. I'm not sure that project is even active anymore.

User Contexts

Every Windows machine has more than one user context, even if there is only one user. For older versions of Windows (and certain server versions), there actually is/was an "Administrator" user in addition to any named users. If you're coming from the *nix world, it's like having a root account and a single user account. On a Mac/Linux, you don't create a root account, the OS just does it. It's the same deal on Windows.

Child Process User Contexts

The most granular way to run a child process while explicitly defining the user context is to run the cmd.exe. So, it might look something like the following (untested) pseudo code:

require('child_process').exec('cmd.exe', ['/c /env /user:username C:\nodejs\node.exe C:\path\to\myscript.js'], function(){...})

You can read more about cmd and runas on technet.

This approach comes with a whole bunch of caveats (which is why I recommend avoiding it). It makes your code platform-specific (i.e. node-mac and node-linux won't work with this). I've also noticed nuances with runas on different versions of Windows, and it can be tough to nail down issues.

Alternatives

A far simpler solution (for anyone new reading this), is to pass the process.env to the child process. In most cases this will work, without all the caveats of runas.

At this point, I'm going to close the issue since it's not really a technical problem with node-windows, but rather a "how you use it". If more discussion/clarification is necessary, it should be directed to StackOverflow using the node-windows tag.