-
Notifications
You must be signed in to change notification settings - Fork 47
GenericExternalService
GenericExternalService
is the implementation of GreengrassService
used for components whose component type is "external" or unspecified. This is the default type for all services.
GenericExternalService
executes processes using native OS calls and has the ability to execute processes as different users and with certain resource limits imposed (Linux only).
When created, GenericExternalService
subscribes to configuration changes for the service in order to react appropriate to changes. In the postInject()
, the service generates a unique token which is used for IPC authentication. This token is only kept in memory and thus it will change any time Greengrass restarts.
GenericExternalService
provides an implementation for isBootstrapRequired
. Bootstrap will be required only if a bootstrap lifecycle step is defined and the version changed or the bootstrap lifecycle definition changed.
To bootstrap()
the service will execute the bootstrap
lifecycle step as defined by the component recipe, if any exists. The bootstrap command will execute with a provided timeout, which will throw an exception if the timeout expires.
To install()
, the service executes the install
lifecycle step synchronously if the step is defined in the component recipe. If the script errors while executing, then the service reports an error which will cause a retry.
To startup()
, the service first caches the shutdown
lifecycle step. This is necessary so that the service can be shutdown using the same shutdown
lifecycle step as was defined when it was launched. Without this caching the service would try to shutdown using the new version of the service's shutdown
lifecycle step which may result in zombie processes, particularly docker-compose containers. The service then executes the startup
lifecycle script if such a script exists. If startup
is defined in the component recipe then Greengrass expects the process started by startup
to either begin a daemon process (which would need to be stopped using shutdown
) and exit with code 0 or a foreground process which uses Greengrass IPC to put the service into the RUNNING
state. If startup
was not defined in the component recipe, then the service will try to execute the run
lifecycle step.
handleRunScript()
takes the service in the STARTING
state into either RUNNING
or FINISHED
. It first will execute the run
script if any script is defined. If it was defined, then the service will go to the RUNNING
state. If it was not defined, then the service will go to the FINISHED
state. And if it was defined, but had an error when launching then the service will go to ERRORED
and retry. run
has an optional timeout which is not specified by default so that it can run indefinitely, but customers can choose to apply a timeout to it.
shutdown()
will kill all processes by first executing any shutdown
lifecycle step if defined. If it was not defined, and even if it was defined, then the service will attempt to stop all remaining processes.
GenericExternalService
's main job is to execute various shell commands as provided in the component recipe, so process execution is quite important to Greengrass. Process control is implemented uniquely for each OS platform by extending the abstract class Exec
. The current two implementations are UnixExec
and WindowsExec
. The base Exec
class does not implement much except for some builder style methods, PATH environment variable setup, generic process startup which calls the abstract method createProcess()
, and stdout/stderr reading threads. Because of the stdout/stderr reading implementation, each process requires a minimum of 2 threads in order to execute. This is not at all ideal and should be improved by using epoll and similar solutions.
UnixExec
has a pretty simple implementation because it is able to just use Java's ProcessBuilder
.
More interesting in this implementation is the close()
method. First, check if already closed, process null, or process no longer running to skip the next work which is expensive. Then, find all child processes and send SIGTERM
to them. Wait up to 5 seconds per child process waiting for it to shutdown (this really should be configurable and certainly longer than 5 seconds by default). Then, forcefully kill any remaining processes using SIGKILL
. And if the thread was interrupted at any point, make sure to clean up as much as possible by killing all child processes.
WindowsExec
has a more complicated implementation than UnixExec
because Windows does not have an easy way for us to execute commands as different users (like we use sudo
on Linux). So for Windows, we need to use JNA to call the Windows native APIs in order to execute commands as the selected user. createProcess
checks if we need to switch users and sets the appropriate fields in ProcessBuilderForWin32
. The implementation of the actual process startup logic is here. The most critical part, and the reason why the custom logic is necessary over just Java's process implementation is to run the process as a different user.
Unlike Unix, Windows requires that we provide the password for the user when running a process as a different user. To do this, Greengrass requires that customers setup a user and store that user's password in the Windows credential store. Greengrass can then read the credential store to get the required password.
close()
will call to either stopGracefully()
or stopForcefully()
. stopGracefully()
tries to emulate SIGTERM
on Unix, but this isn't a feature of Windows and the closest thing is to send a ctrl+c event to the process's console. Achieving this is unfortunately complicated. Greengrass first spawns a new process whose purpose is just to keep the console that Greengrass was using alive. We then detach from our existing console and attach to the console of the process we're trying to stop. Next, we need to remove Java's ctrl+c handler so that Greengrass won't shutdown in response to the ctrl+c that we're about to send. Then we send the ctrl+c event, this should result in the process that we're trying to stop receiving the event and then taking action to shutdown. Now that the event has been sent, we need to reset Greengrass back to the initial state. Detach from the console we just sent ctrl+c to, pause for 1250 ms, reattach to our original console, and finally set a new ctrl+c handler for Greengrass so that Greengrass can still be shutdown gracefully.
To stop a process forcefully (equivalent to SIGKILL
on Unix), we use the taskkill
command with /F
.