-
Notifications
You must be signed in to change notification settings - Fork 5.5k
[Windows] Onboard Envoy to SCM #14352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 29 commits
207e376
e83f55b
b96ed04
e82dc54
6766c9a
aba15d9
a753e67
2d49561
cc0e28c
7c4eb50
12f5325
ea368eb
5d1aed8
fe474b1
0859975
e7c2c41
92a683f
02148e3
37a4d68
a370860
853d43e
0df02a3
4274d5c
ad75015
439811b
44ce57b
e6668dd
9ddf9b2
c8f664a
6650ba0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,9 @@ | ||
| #include "exe/main_common.h" | ||
|
|
||
| #ifdef WIN32 | ||
| #include "exe/service_base.h" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am a new hand to envoy, but there seems no
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you encountering any issues compiling this or just curious? We use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious about it, thanks a lot for your help : )
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No prob, happy to help. |
||
| #endif | ||
|
|
||
| // NOLINT(namespace-envoy) | ||
|
|
||
| /** | ||
|
|
@@ -9,4 +13,13 @@ | |
| * deployment such as initializing signal handling. It calls main_common | ||
| * after setting up command line options. | ||
| */ | ||
| int main(int argc, char** argv) { return Envoy::MainCommon::main(argc, argv); } | ||
| int main(int argc, char** argv) { | ||
| #ifdef WIN32 | ||
| Envoy::ServiceBase service; | ||
| if (!Envoy::ServiceBase::TryRunAsService(service)) { | ||
| return Envoy::MainCommon::main(argc, argv); | ||
| } | ||
| return EXIT_SUCCESS; | ||
| #endif | ||
| return Envoy::MainCommon::main(argc, argv); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| #include <codecvt> | ||
| #include <locale> | ||
|
|
||
| #include "common/buffer/buffer_impl.h" | ||
| #include "common/common/assert.h" | ||
| #include "common/event/signal_impl.h" | ||
|
|
||
| #include "exe/main_common.h" | ||
| #include "exe/service_base.h" | ||
|
|
||
| #include "absl/debugging/symbolize.h" | ||
|
|
||
| namespace Envoy { | ||
|
|
||
| namespace { | ||
| DWORD Win32FromHResult(HRESULT value) { return value & ~0x80070000; } | ||
|
|
||
| } // namespace | ||
|
|
||
| ServiceBase* ServiceBase::service_static = nullptr; | ||
|
|
||
| ServiceBase::ServiceBase(DWORD controlsAccepted) : handle_(0) { | ||
| status_.dwServiceType = SERVICE_WIN32_OWN_PROCESS; | ||
| status_.dwCurrentState = SERVICE_START_PENDING; | ||
| status_.dwControlsAccepted = controlsAccepted; | ||
| } | ||
|
|
||
| bool ServiceBase::TryRunAsService(ServiceBase& service) { | ||
| // We need to be extremely defensive when programming between the start of the program until | ||
| // `main_common` starts because the loggers have not been initialized | ||
| // so we do not have a good way to know what is happening if the program fails. | ||
| service_static = &service; | ||
|
|
||
| // The `SERVICE_TABLE_ENTRY` struct requires a volatile `LPSTR` | ||
| char nullstr[1] = ""; | ||
| SERVICE_TABLE_ENTRYA service_table[] = {// Even though the service name is ignored for own process | ||
| // services, it must be a valid string and cannot be 0. | ||
| {nullstr, (LPSERVICE_MAIN_FUNCTIONA)ServiceMain}, | ||
| // Designates the end of table. | ||
| {0, 0}}; | ||
|
|
||
| if (!::StartServiceCtrlDispatcherA(service_table)) { | ||
| auto last_error = ::GetLastError(); | ||
| // There are two cases: | ||
| // 1. The user is trying to run Envoy as a console app. In that the last error is | ||
| // `ERROR_FAILED_SERVICE_CONTROLLER_CONNECT`. In that case we return false and let Envoy start | ||
| // as normal. | ||
| // 2. Other programmatic errors. | ||
| if (last_error == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) { | ||
|
davinci26 marked this conversation as resolved.
|
||
| return false; | ||
| } else { | ||
| PANIC( | ||
| fmt::format("Could not dispatch Envoy to start as a service with error {}", last_error)); | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| DWORD ServiceBase::Start(std::vector<std::string> args) { | ||
| // Run the server listener loop outside try/catch blocks, so that unexpected exceptions | ||
| // show up as a core-dumps for easier diagnostics. | ||
| absl::InitializeSymbolizer(args[0].c_str()); | ||
| std::shared_ptr<Envoy::MainCommon> main_common; | ||
|
|
||
| // Initialize the server's main context under a try/catch loop and simply return `EXIT_FAILURE` | ||
| // as needed. Whatever code in the initialization path that fails is expected to log an error | ||
| // message so the user can diagnose. | ||
| try { | ||
| main_common = std::make_shared<Envoy::MainCommon>(args); | ||
| Envoy::Server::Instance* server = main_common->server(); | ||
| if (!server->options().signalHandlingEnabled()) { | ||
| // This means that the Envoy has not registered `ENVOY_SIGTERM`. | ||
| // we need to manually enable it as we are going to use it to | ||
| // handle close requests from `SCM`. | ||
| sigterm_ = server->dispatcher().listenForSignal(ENVOY_SIGTERM, [server]() { | ||
| ENVOY_LOG_MISC(warn, "caught ENVOY_SIGTERM"); | ||
| server->shutdown(); | ||
| }); | ||
| } | ||
| } catch (const Envoy::NoServingException& e) { | ||
| return S_OK; | ||
| } catch (const Envoy::MalformedArgvException& e) { | ||
| return E_INVALIDARG; | ||
| } catch (const Envoy::EnvoyException& e) { | ||
| ENVOY_LOG_MISC(warn, "Envoy failed to start with {}", e.what()); | ||
| return E_FAIL; | ||
| } | ||
|
|
||
| return main_common->run() ? S_OK : E_FAIL; | ||
| } | ||
|
|
||
| void ServiceBase::Stop(DWORD control) { | ||
| auto handler = Event::eventBridgeHandlersSingleton::get()[ENVOY_SIGTERM]; | ||
| if (!handler) { | ||
| PANIC("No handler is registered to stop gracefully, aborting the program."); | ||
| } | ||
|
|
||
| char data[] = {'a'}; | ||
| Buffer::RawSlice buffer{data, 1}; | ||
| auto result = handler->writev(&buffer, 1); | ||
| RELEASE_ASSERT(result.rc_ == 1, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @davinci26 how come we are using
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or maybe it's fine because you are on the way out anyway? I'd be interested in your take @davinci26 on all the RELEASE_ASSERTs in this module relative to the policy. I actually think it might be fine, but it's hard to tell without understanding the Windows API expectations.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Exactly, we have already received a SIGTERM from the service control manager which means that for some reason we need to shutdown anyway. We try to do it in a graceful way but if the write fails we might as well crash. The rest of the release asserts are guarding against a developer shooting themselves at the foot. i.e. someone manually calling delete the pointers on the global scope. I added the release asserts for them based on Do you want me to document these in code with comments so it's clear for anyone reading the code? |
||
| fmt::format("failed to write 1 byte: {}", result.err_->getErrorDetails())); | ||
| } | ||
|
|
||
| void ServiceBase::UpdateState(DWORD state, HRESULT errorCode, bool serviceError) { | ||
| status_.dwCurrentState = state; | ||
| if (FAILED(errorCode)) { | ||
| if (!serviceError) { | ||
| status_.dwWin32ExitCode = Win32FromHResult(errorCode); | ||
| } else { | ||
| status_.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR; | ||
| status_.dwServiceSpecificExitCode = errorCode; | ||
| } | ||
| } | ||
| SetServiceStatus(); | ||
| } | ||
|
|
||
| void ServiceBase::SetServiceStatus() { | ||
| RELEASE_ASSERT(service_static != nullptr, "Global pointer to service should not be null"); | ||
| if (!::SetServiceStatus(handle_, &status_)) { | ||
| PANIC( | ||
| fmt::format("Could not start StartServiceCtrlDispatcher with error {}", ::GetLastError())); | ||
| } | ||
| } | ||
|
|
||
| void WINAPI ServiceBase::ServiceMain(DWORD argc, LPSTR* argv) { | ||
| RELEASE_ASSERT(service_static != nullptr, "Global pointer to service should not be null"); | ||
| if (argc < 1 || argv == 0 || argv[0] == 0) { | ||
| service_static->UpdateState(SERVICE_STOPPED, E_INVALIDARG, true); | ||
| } | ||
|
|
||
| service_static->handle_ = ::RegisterServiceCtrlHandlerA("ENVOY\0", Handler); | ||
| if (service_static->handle_ == 0) { | ||
| service_static->UpdateState(SERVICE_STOPPED, ::GetLastError(), false); | ||
| } | ||
|
|
||
| // Windows Services can get their arguments in two different ways | ||
| // 1. With command line arguments that have been registered when the service gets created. | ||
| // 2. With arguments coming from StartServiceA. | ||
| // We merge the two cases into one vector of arguments that we provide to main common. | ||
|
davinci26 marked this conversation as resolved.
|
||
| auto cli = std::wstring(::GetCommandLineW()); | ||
| int envoyArgCount = 0; | ||
| LPWSTR* argvEnvoy = CommandLineToArgvW(cli.c_str(), &envoyArgCount); | ||
| std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter; | ||
| std::vector<std::string> args; | ||
| args.reserve(envoyArgCount + argc - 1); | ||
| for (int i = 0; i < envoyArgCount; ++i) { | ||
| args.emplace_back(converter.to_bytes(std::wstring(argvEnvoy[i]))); | ||
| } | ||
|
|
||
| for (int i = 1; i < argc; i++) { | ||
| args.emplace_back(std::string(argv[i])); | ||
| } | ||
|
|
||
| service_static->SetServiceStatus(); | ||
| service_static->UpdateState(SERVICE_RUNNING); | ||
| DWORD rc = service_static->Start(args); | ||
| service_static->UpdateState(SERVICE_STOPPED, rc, true); | ||
| } | ||
|
|
||
| void WINAPI ServiceBase::Handler(DWORD control) { | ||
| // When the service control manager sends a control code to a service, it waits for the handler | ||
| // function to return before sending additional control codes to other services. | ||
| // The control handler should return as quickly as possible; if it does not return within 30 | ||
| // seconds, the SCM returns an error. If a service must do lengthy processing when the service is | ||
| // executing the control handler, it should create a secondary thread to perform the lengthy | ||
| // processing, and then return from the control handler. | ||
| switch (control) { | ||
| case SERVICE_CONTROL_SHUTDOWN: | ||
| case SERVICE_CONTROL_PRESHUTDOWN: | ||
| case SERVICE_CONTROL_STOP: { | ||
| ENVOY_BUG(service_static->status_.dwCurrentState == SERVICE_RUNNING, | ||
| "Attempting to stop Envoy service when it is not running"); | ||
| service_static->UpdateState(SERVICE_STOP_PENDING); | ||
| service_static->Stop(control); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| #pragma once | ||
|
|
||
| #include <processenv.h> | ||
| #include <shellapi.h> | ||
| #include <winsvc.h> | ||
|
|
||
| #include <functional> | ||
| #include <string> | ||
|
|
||
| #include "envoy/event/signal.h" | ||
|
|
||
| #include "exe/service_status.h" | ||
|
|
||
| namespace Envoy { | ||
| class ServiceBase { | ||
| public: | ||
| virtual ~ServiceBase() = default; | ||
| /** | ||
| * Try to register Envoy as a service. For this we register `ServiceMain` and `Handler` | ||
| * @param ServiceBase instance of the service. | ||
| * @return false if Envoy can not be registered to run as service. | ||
| */ | ||
| static bool TryRunAsService(ServiceBase& service); | ||
|
davinci26 marked this conversation as resolved.
|
||
|
|
||
| ServiceBase(DWORD controlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN); | ||
| /** | ||
| * Start the service. | ||
| * @return exit code of Envoy. | ||
| */ | ||
| DWORD Start(std::vector<std::string> args); | ||
|
|
||
| /** | ||
| * Stop the service. | ||
| * @param control the control code Service Control Manager requested. | ||
| */ | ||
| void Stop(DWORD control); | ||
|
|
||
| /** | ||
| * Update the state of the service. | ||
| * @param state New service state. | ||
| * @param errorCode Last error code encountered by Envoy. | ||
| * @param serviceError If the error is coming from the service or a Win32 interaction. | ||
| */ | ||
| void UpdateState(DWORD state, HRESULT errorCode = S_OK, bool serviceError = true); | ||
|
|
||
| private: | ||
| void SetServiceStatus(); | ||
|
|
||
| static void WINAPI ServiceMain(DWORD argc, LPSTR* argv); | ||
|
|
||
| static void WINAPI Handler(DWORD control); | ||
|
|
||
| static ServiceBase* service_static; | ||
| SERVICE_STATUS_HANDLE handle_; | ||
| ServiceStatus status_; | ||
| Event::SignalEventPtr sigterm_; | ||
| }; | ||
| } // namespace Envoy | ||
Uh oh!
There was an error while loading. Please reload this page.