diff --git a/.gitignore b/.gitignore index bcf7e98552e..f2a7dc839da 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ out/ coverage/ _bin/ *.exe +*.exe~ *.zip *.swp *.orig @@ -15,3 +16,4 @@ _bin/ /misc/windows-iam/devcon* /misc/pause-container/pause *-stamp +/.idea/ diff --git a/README.md b/README.md index 010fb1db7bc..942a0cd9d6e 100644 --- a/README.md +++ b/README.md @@ -48,26 +48,68 @@ See also the Advanced Usage section below. ### On Windows Server 2016 -On Windows Server 2016, the Amazon ECS Container Agent runs as a process on the -host. Unlike Linux, the agent may not run inside a container as it uses the -host's registry and the named pipe at `\\.\pipe\docker_engine` to communicate -with the Docker daemon. +On Windows Server 2016, the Amazon ECS Container Agent runs as a process or +service on the host. Unlike Linux, the agent may not run inside a container as +it uses the host's registry and the named pipe at `\\.\pipe\docker_engine` to +communicate with the Docker daemon. + +#### As a Service +To install the service, you can do the following: + +```powershell +PS C:\> # Set up directories the agent uses +PS C:\> New-Item -Type directory -Path ${env:ProgramFiles}\Amazon\ECS -Force +PS C:\> New-Item -Type directory -Path ${env:ProgramData}\Amazon\ECS -Force +PS C:\> # Set up configuration +PS C:\> $ecsExeDir = "${env:ProgramFiles}\Amazon\ECS" +PS C:\> [Environment]::SetEnvironmentVariable("ECS_CLUSTER", "my-windows-cluster", "Machine") +PS C:\> [Environment]::SetEnvironmentVariable("ECS_LOGFILE", "${env:ProgramData}\Amazon\ECS\log\ecs-agent.log", "Machine") +PS C:\> [Environment]::SetEnvironmentVariable("ECS_DATADIR", "${env:ProgramData}\Amazon\ECS\data", "Machine") +PS C:\> # Download the agent +PS C:\> $agentVersion = "latest" +PS C:\> $agentZipUri = "https://s3.amazonaws.com/amazon-ecs-agent/ecs-agent-windows-$agentVersion.zip" +PS C:\> $zipFile = "${env:TEMP}\ecs-agent.zip" +PS C:\> Invoke-RestMethod -OutFile $zipFile -Uri $agentZipUri +PS C:\> # Put the executables in the executable directory. +PS C:\> Expand-Archive -Path $zipFile -DestinationPath $ecsExeDir -Force +PS C:\> Set-Location ${ecsExeDir} +PS C:\> # Set $EnableTaskIAMRoles to $true to enable task IAM roles +PS C:\> # Note that enabling IAM roles will make port 80 unavailable for tasks. +PS C:\> [bool]$EnableTaskIAMRoles = $false +PS C:\> if (${EnableTaskIAMRoles} { +>> .\hostsetup.ps1 +>> } +PS C:\> # Install the agent service +PS C:\> New-Service -Name "AmazonECS" ` + -BinaryPathName "$ecsExeDir\amazon-ecs-agent.exe -windows-service" ` + -DisplayName "Amazon ECS" ` + -Description "Amazon ECS service runs the Amazon ECS agent" ` + -DependsOn Docker ` + -StartupType Manual +``` + +To run the service, you can do the following: +```powershell +Start-Service AmazonECS +``` + +#### As a Process ```powershell PS C:\> # Set up directories the agent uses -PS C:\> New-Item -Type directory -Path $ProgramFiles\Amazon\ECS -PS C:\> New-Item -Type directory -Path $ProgramData\Amazon\ECS +PS C:\> New-Item -Type directory -Path ${env:ProgramFiles}\Amazon\ECS -Force +PS C:\> New-Item -Type directory -Path ${env:ProgramData}\Amazon\ECS -Force PS C:\> # Set up configuration -PS C:\> $ecsExeDir = "$env:ProgramFiles\Amazon\ECS" +PS C:\> $ecsExeDir = "${env:ProgramFiles}\Amazon\ECS" PS C:\> [Environment]::SetEnvironmentVariable("ECS_CLUSTER", "my-windows-cluster", "Machine") -PS C:\> [Environment]::SetEnvironmentVariable("ECS_LOGFILE", "$ProgramData\Amazon\ECS\log\ecs-agent.log", "Machine") -PS C:\> [Environment]::SetEnvironmentVariable("ECS_DATADIR", "$ProgramData\Amazon\ECS\data", "Machine") +PS C:\> [Environment]::SetEnvironmentVariable("ECS_LOGFILE", "${env:ProgramData}\Amazon\ECS\log\ecs-agent.log", "Machine") +PS C:\> [Environment]::SetEnvironmentVariable("ECS_DATADIR", "${env:ProgramData}\Amazon\ECS\data", "Machine") PS C:\> # Set this environment variable to "true" to enable IAM roles. Note that enabling IAM roles will make port 80 unavailable for tasks. PS C:\> [Environment]::SetEnvironmentVariable("ECS_ENABLE_TASK_IAM_ROLE", "false", "Machine") PS C:\> # Download the agent PS C:\> $agentVersion = "latest" PS C:\> $agentZipUri = "https://s3.amazonaws.com/amazon-ecs-agent/ecs-agent-windows-$agentVersion.zip" -PS C:\> $zipFile = "$env:TEMP\ecs-agent.zip" +PS C:\> $zipFile = "${env:TEMP}\ecs-agent.zip" PS C:\> Invoke-RestMethod -OutFile $zipFile -Uri $agentZipUri PS C:\> # Put the executables in the executable directory. PS C:\> Expand-Archive -Path $zipFile -DestinationPath $ecsExeDir -Force diff --git a/agent/Gopkg.lock b/agent/Gopkg.lock index 19d84fa88ed..570e0f81d95 100644 --- a/agent/Gopkg.lock +++ b/agent/Gopkg.lock @@ -143,7 +143,7 @@ [[projects]] name = "golang.org/x/sys" - packages = ["unix","windows","windows/registry"] + packages = ["unix","windows","windows/registry","windows/svc","windows/svc/eventlog"] revision = "afadfcc7779c1f4db0f6f6438afcb108d9c9c7cd" [[projects]] @@ -154,6 +154,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "7622bf3f939a3ea183688145cbae7aee8a0513f27191096a17be1956d727ea36" + inputs-digest = "c09c3275be0ca13e00eca77147f1689e0d619e64b233e3dfdbda494c09215118" solver-name = "gps-cdcl" solver-version = 1 diff --git a/agent/app/agent.go b/agent/app/agent.go index c3490398e50..948ff843283 100644 --- a/agent/app/agent.go +++ b/agent/app/agent.go @@ -46,9 +46,9 @@ import ( "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/aws/amazon-ecs-agent/agent/version" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" aws_credentials "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/defaults" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/cihub/seelog" ) @@ -76,8 +76,12 @@ type agent interface { // printECSAttributes prints the Agent's capabilities based on // its environment printECSAttributes() int + // startWindowsService starts the agent as a Windows Service + startWindowsService() int // start starts the Agent execution start() int + // setTerminationHandler sets the termination handler + setTerminationHandler(sighandlers.TerminationHandler) } // ecsAgent wraps all the entities needed to start the ECS Agent execution. @@ -100,9 +104,10 @@ type ecsAgent struct { mac string metadataManager containermetadata.Manager resource resources.Resource + terminationHandler sighandlers.TerminationHandler } -// newAgent returns a new ecsAgent object +// newAgent returns a new ecsAgent object, but does not start anything func newAgent( ctx context.Context, blackholeEC2Metadata bool, @@ -158,9 +163,10 @@ func newAgent( PluginsPath: cfg.CNIPluginsPath, MinSupportedCNIVersion: config.DefaultMinSupportedCNIVersion, }), - os: oswrapper.New(), - metadataManager: metadataManager, - resource: resources.New(), + os: oswrapper.New(), + metadataManager: metadataManager, + resource: resources.New(), + terminationHandler: sighandlers.StartDefaultTerminationHandler, }, nil } @@ -184,6 +190,10 @@ func (agent *ecsAgent) printECSAttributes() int { return exitcodes.ExitSuccess } +func (agent *ecsAgent) setTerminationHandler(handler sighandlers.TerminationHandler) { + agent.terminationHandler = handler +} + // start starts the ECS Agent func (agent *ecsAgent) start() int { sighandlers.StartDebugHandler() @@ -514,7 +524,7 @@ func (agent *ecsAgent) startAsyncRoutines( go imageManager.StartImageCleanupProcess(agent.ctx) } - go sighandlers.StartTerminationHandler(stateManager, taskEngine) + go agent.terminationHandler(stateManager, taskEngine) // Agent introspection api go handlers.ServeHttp(&agent.containerInstanceARN, taskEngine, agent.cfg) diff --git a/agent/app/agent_unix.go b/agent/app/agent_unix.go index 5575363e33d..b8ff77fa815 100644 --- a/agent/app/agent_unix.go +++ b/agent/app/agent_unix.go @@ -41,6 +41,12 @@ var awsVPCCNIPlugins = []string{ecscni.ECSENIPluginName, ecscni.ECSIPAMPluginName, } +// startWindowsService is not supported on Linux +func (agent *ecsAgent) startWindowsService() int { + seelog.Error("Windows Services are not supported on Linux") + return 1 +} + // initializeTaskENIDependencies initializes all of the dependencies required by // the Agent to support the 'awsvpc' networking mode. A non nil error is returned // if an error is encountered during this process. An additional boolean flag to diff --git a/agent/app/agent_unix_test.go b/agent/app/agent_unix_test.go index 7e43ef6859c..605a681bc7a 100644 --- a/agent/app/agent_unix_test.go +++ b/agent/app/agent_unix_test.go @@ -39,6 +39,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/eventstream" "github.com/aws/amazon-ecs-agent/agent/resources/mock_resources" "github.com/aws/amazon-ecs-agent/agent/sighandlers/exitcodes" + "github.com/aws/amazon-ecs-agent/agent/statemanager" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/golang/mock/gomock" @@ -100,6 +101,7 @@ func TestDoStartHappyPath(t *testing.T) { cfg: &cfg, credentialProvider: credentials.NewCredentials(mockCredentialsProvider), dockerClient: dockerClient, + terminationHandler: func(saver statemanager.Saver, taskEngine engine.TaskEngine) {}, } go agent.doStart(eventstream.NewEventStream("events", ctx), @@ -198,6 +200,7 @@ func TestDoStartTaskENIHappyPath(t *testing.T) { cniClient: cniClient, os: mockOS, ec2MetadataClient: mockMetadata, + terminationHandler: func(saver statemanager.Saver, taskEngine engine.TaskEngine) {}, } go agent.doStart(eventstream.NewEventStream("events", ctx), @@ -497,6 +500,7 @@ func TestDoStartCgroupInitHappyPath(t *testing.T) { credentialProvider: credentials.NewCredentials(mockCredentialsProvider), dockerClient: dockerClient, resource: mockResource, + terminationHandler: func(saver statemanager.Saver, taskEngine engine.TaskEngine) {}, } go agent.doStart(eventstream.NewEventStream("events", ctx), @@ -537,6 +541,7 @@ func TestDoStartCgroupInitErrorPath(t *testing.T) { credentialProvider: credentials.NewCredentials(mockCredentialsProvider), dockerClient: dockerClient, resource: mockResource, + terminationHandler: func(saver statemanager.Saver, taskEngine engine.TaskEngine) {}, } status := agent.doStart(eventstream.NewEventStream("events", ctx), diff --git a/agent/app/agent_windows.go b/agent/app/agent_windows.go new file mode 100644 index 00000000000..1e2ecfca564 --- /dev/null +++ b/agent/app/agent_windows.go @@ -0,0 +1,226 @@ +// +build windows + +// Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package app + +import ( + "context" + "sync" + "time" + + "github.com/aws/amazon-ecs-agent/agent/engine" + "github.com/aws/amazon-ecs-agent/agent/sighandlers" + "github.com/aws/amazon-ecs-agent/agent/statemanager" + "github.com/cihub/seelog" + "golang.org/x/sys/windows/svc" +) + +const ( + //EcsSvcName is the name of the service + EcsSvcName = "AmazonECS" +) + +// startWindowsService runs the ECS agent as a Windows Service +func (agent *ecsAgent) startWindowsService() int { + svc.Run(EcsSvcName, newHandler(agent)) + return 0 +} + +// handler implements https://godoc.org/golang.org/x/sys/windows/svc#Handler +type handler struct { + ecsAgent agent +} + +func newHandler(agent agent) *handler { + return &handler{agent} +} + +// Execute implements https://godoc.org/golang.org/x/sys/windows/svc#Handler +// The basic way that this implementation works is through two channels (representing the requests from Windows and the +// responses we're sending to Windows) and two goroutines (one for message processing with Windows and the other to +// actually run the agent). Once we've set everything up and started both goroutines, we wait for either one to exit +// (the Windows goroutine will exit based on messages from Windows while the agent goroutine exits if the agent exits) +// and then cancel the other. Once everything has stopped running, this function returns and the process exits. +func (h *handler) Execute(args []string, requests <-chan svc.ChangeRequest, responses chan<- svc.Status) (bool, uint32) { + // channels for communication between goroutines + ctx, cancel := context.WithCancel(context.Background()) + agentDone := make(chan struct{}) + windowsDone := make(chan struct{}) + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + defer close(windowsDone) + defer wg.Done() + h.handleWindowsRequests(ctx, requests, responses) + }() + + go func() { + defer close(agentDone) + defer wg.Done() + h.runAgent(ctx) + }() + + // Wait until one of the goroutines is either told to stop or fails spectacularly. Under normal conditions we will + // be waiting here for a long time. + select { + case <-windowsDone: + // Service was told to stop by the Windows API. This happens either through manual intervention (i.e., + // "Stop-Service AmazonECS") or through system shutdown. Regardless, this is a normal exit event and not an + // error. + seelog.Info("Received normal signal from Windows to exit") + case <-agentDone: + // This means that the agent stopped on its own. This is where it's appropriate to light the event log on fire + // and set off all the alarms. + seelog.Error("Exiting") + } + cancel() + wg.Wait() + return true, 0 +} + +// handleWindowsRequests is a loop intended to run in a goroutine. It handles bidirectional communication with the +// Windows service manager. This function works by pretty much immediately moving to running and then waiting for a +// stop or shut down message from Windows or to be canceled (which could happen if the agent exits by itself and the +// calling function cancels the context). +func (h *handler) handleWindowsRequests(ctx context.Context, requests <-chan svc.ChangeRequest, responses chan<- svc.Status) { + // Immediately tell Windows that we are pending service start. + responses <- svc.Status{State: svc.StartPending} + seelog.Info("Starting Windows service") + + // TODO: Pre-start hooks go here (unclear if we need any yet) + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms682108(v=vs.85).aspx + // Not sure if a better link exists to describe what these values mean + accepts := svc.AcceptStop | svc.AcceptShutdown + + // Announce that we are running and we accept the above-mentioned commands + responses <- svc.Status{State: svc.Running, Accepts: accepts} + + defer func() { + // Announce that we are stopping + seelog.Info("Stopping Windows service") + responses <- svc.Status{State: svc.StopPending} + }() + + for { + select { + case <-ctx.Done(): + return + case r := <-requests: + switch r.Cmd { + case svc.Interrogate: + // Our status doesn't change unless we are told to stop or shutdown + responses <- r.CurrentStatus + case svc.Stop, svc.Shutdown: + return + default: + continue + } + } + } +} + +// runAgent runs the ECS agent inside a goroutine and waits to be told to exit. +func (h *handler) runAgent(ctx context.Context) { + agentCtx, cancel := context.WithCancel(ctx) + indicator := newTermHandlerIndicator() + + terminationHandler := func(saver statemanager.Saver, taskEngine engine.TaskEngine) { + // We're using a custom indicator to record that the handler is scheduled to be executed (has been invoked) and + // to determine whether it should run (we skip when the agent engine has already exited). After recording to + // the indicator that the handler has been invoked, we wait on the context. When we wake up, we determine + // whether to execute or not based on whether the agent is still running. + defer indicator.done() + indicator.setInvoked() + <-agentCtx.Done() + if !indicator.isAgentRunning() { + return + } + + seelog.Info("Termination handler received signal to stop") + err := sighandlers.FinalSave(saver, taskEngine) + if err != nil { + seelog.Criticalf("Error saving state before final shutdown: %v", err) + } + } + h.ecsAgent.setTerminationHandler(terminationHandler) + + go func() { + h.ecsAgent.start() // should block forever, unless there is an error + // TODO: distinguish between recoverable and unrecoverable errors + indicator.agentStopped() + cancel() + }() + + sleepCtx(agentCtx, time.Minute) // give the agent a minute to start and invoke terminationHandler + + // wait for the termination handler to run. Once the termination handler runs, we can safely exit. If the agent + // exits by itself, the termination handler doesn't need to do anything and skips. If the agent exits before the + // termination handler is invoked, we can exit immediately. + indicator.wait() +} + +// sleepCtx provides a cancelable sleep +func sleepCtx(ctx context.Context, duration time.Duration) { + derivedCtx, _ := context.WithDeadline(ctx, time.Now().Add(duration)) + <-derivedCtx.Done() +} + +type termHandlerIndicator struct { + mu sync.Mutex + agentRunning bool + handlerInvoked bool + handlerDone chan struct{} +} + +func newTermHandlerIndicator() *termHandlerIndicator { + return &termHandlerIndicator{ + agentRunning: true, + handlerInvoked: false, + handlerDone: make(chan struct{}), + } +} + +func (t *termHandlerIndicator) isAgentRunning() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.agentRunning +} + +func (t *termHandlerIndicator) agentStopped() { + t.mu.Lock() + defer t.mu.Unlock() + t.agentRunning = false +} + +func (t *termHandlerIndicator) done() { + close(t.handlerDone) +} + +func (t *termHandlerIndicator) setInvoked() { + t.mu.Lock() + defer t.mu.Unlock() + t.handlerInvoked = true +} + +func (t *termHandlerIndicator) wait() { + t.mu.Lock() + invoked := t.handlerInvoked + t.mu.Unlock() + if invoked { + <-t.handlerDone + } +} diff --git a/agent/app/agent_windows_test.go b/agent/app/agent_windows_test.go index 9dbe4ebe975..9885c601f33 100644 --- a/agent/app/agent_windows_test.go +++ b/agent/app/agent_windows_test.go @@ -19,12 +19,18 @@ import ( "context" "sync" "testing" + "time" app_mocks "github.com/aws/amazon-ecs-agent/agent/app/mocks" "github.com/aws/amazon-ecs-agent/agent/engine" "github.com/aws/amazon-ecs-agent/agent/eventstream" + "github.com/aws/amazon-ecs-agent/agent/sighandlers" + "github.com/aws/amazon-ecs-agent/agent/statemanager" + statemanager_mocks "github.com/aws/amazon-ecs-agent/agent/statemanager/mocks" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "golang.org/x/sys/windows/svc" ) // TestDoStartHappyPath tests the doStart method for windows. This method should @@ -71,6 +77,7 @@ func TestDoStartHappyPath(t *testing.T) { cfg: &cfg, credentialProvider: credentials.NewCredentials(mockCredentialsProvider), dockerClient: dockerClient, + terminationHandler: func(saver statemanager.Saver, taskEngine engine.TaskEngine) {}, } go agent.doStart(eventstream.NewEventStream("events", ctx), @@ -81,3 +88,242 @@ func TestDoStartHappyPath(t *testing.T) { // NewSession call has been invoked discoverEndpointsInvoked.Wait() } + +type mockAgent struct { + startFunc func() int + terminationHandler sighandlers.TerminationHandler +} + +func (m *mockAgent) start() int { + return m.startFunc() +} +func (m *mockAgent) setTerminationHandler(handler sighandlers.TerminationHandler) { + m.terminationHandler = handler +} +func (m *mockAgent) printVersion() int { return 0 } +func (m *mockAgent) printECSAttributes() int { return 0 } +func (m *mockAgent) startWindowsService() int { return 0 } + +func TestHandler_RunAgent_StartExitImmediately(t *testing.T) { + // register some mocks, but nothing should get called on any of them + ctrl := gomock.NewController(t) + _ = statemanager_mocks.NewMockStateManager(ctrl) + _ = engine.NewMockTaskEngine(ctrl) + defer ctrl.Finish() + + wg := sync.WaitGroup{} + wg.Add(1) + startFunc := func() int { + // startFunc doesn't block, nothing is called + wg.Done() + return 0 + } + agent := &mockAgent{startFunc: startFunc} + handler := &handler{agent} + go handler.runAgent(context.TODO()) + wg.Wait() + assert.NotNil(t, agent.terminationHandler) +} + +func TestHandler_RunAgent_NoSaveWithNoTerminationHandler(t *testing.T) { + // register some mocks, but nothing should get called on any of them + ctrl := gomock.NewController(t) + _ = statemanager_mocks.NewMockStateManager(ctrl) + _ = engine.NewMockTaskEngine(ctrl) + defer ctrl.Finish() + + done := make(chan struct{}) + startFunc := func() int { + <-done // block until after the test ends so that we can test that runAgent returns when cancelled + return 0 + } + agent := &mockAgent{startFunc: startFunc} + handler := &handler{agent} + ctx, cancel := context.WithCancel(context.TODO()) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + handler.runAgent(ctx) + wg.Done() + }() + cancel() + wg.Wait() + assert.NotNil(t, agent.terminationHandler) +} + +func TestHandler_RunAgent_ForceSaveWithTerminationHandler(t *testing.T) { + ctrl := gomock.NewController(t) + stateManager := statemanager_mocks.NewMockStateManager(ctrl) + taskEngine := engine.NewMockTaskEngine(ctrl) + defer ctrl.Finish() + + taskEngine.EXPECT().Disable() + stateManager.EXPECT().ForceSave() + + agent := &mockAgent{} + + done := make(chan struct{}) + defer func() { done <- struct{}{} }() + startFunc := func() int { + go agent.terminationHandler(stateManager, taskEngine) + <-done // block until after the test ends so that we can test that runAgent returns when cancelled + return 0 + } + agent.startFunc = startFunc + handler := &handler{agent} + ctx, cancel := context.WithCancel(context.TODO()) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + handler.runAgent(ctx) + wg.Done() + }() + time.Sleep(time.Second) // give startFunc enough time to actually call the termination handler + cancel() + wg.Wait() +} + +func TestHandler_HandleWindowsRequests_StopService(t *testing.T) { + requests := make(chan svc.ChangeRequest) + responses := make(chan svc.Status) + + handler := &handler{} + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + handler.handleWindowsRequests(context.TODO(), requests, responses) + wg.Done() + }() + + go func() { + resp := <-responses + assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately") + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending") + assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown") + requests <- svc.ChangeRequest{Cmd: svc.Interrogate, CurrentStatus: svc.Status{State: svc.Running}} + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after Interrogate") + requests <- svc.ChangeRequest{Cmd: svc.Stop} + resp = <-responses + assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after Stop") + wg.Done() + }() + + wg.Wait() +} + +func TestHandler_HandleWindowsRequests_Cancel(t *testing.T) { + requests := make(chan svc.ChangeRequest) + responses := make(chan svc.Status) + + handler := &handler{} + ctx, cancel := context.WithCancel(context.TODO()) + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + handler.handleWindowsRequests(ctx, requests, responses) + wg.Done() + }() + + go func() { + resp := <-responses + assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately") + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending") + assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown") + requests <- svc.ChangeRequest{Cmd: svc.Interrogate, CurrentStatus: svc.Status{State: svc.Running}} + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after Interrogate") + cancel() + resp = <-responses + assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after Cancel") + wg.Done() + }() + + wg.Wait() +} + +func TestHandler_Execute_WindowsStops(t *testing.T) { + ctrl := gomock.NewController(t) + stateManager := statemanager_mocks.NewMockStateManager(ctrl) + taskEngine := engine.NewMockTaskEngine(ctrl) + defer ctrl.Finish() + + taskEngine.EXPECT().Disable() + stateManager.EXPECT().ForceSave() + + agent := &mockAgent{} + + done := make(chan struct{}) + defer func() { done <- struct{}{} }() + startFunc := func() int { + go agent.terminationHandler(stateManager, taskEngine) + <-done // block until after the test ends so that we can test that Execute returns when Stopped + return 0 + } + agent.startFunc = startFunc + handler := &handler{agent} + requests := make(chan svc.ChangeRequest) + responses := make(chan svc.Status) + + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + handler.Execute(nil, requests, responses) + wg.Done() + }() + + go func() { + resp := <-responses + assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately") + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending") + assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown") + time.Sleep(time.Second) // let it run for a second + requests <- svc.ChangeRequest{Cmd: svc.Shutdown} + resp = <-responses + assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after Shutdown") + wg.Done() + }() + + wg.Wait() +} + +func TestHandler_Execute_AgentStops(t *testing.T) { + agent := &mockAgent{} + + ctx, cancel := context.WithCancel(context.TODO()) + startFunc := func() int { + <-ctx.Done() + return 0 + } + agent.startFunc = startFunc + handler := &handler{agent} + requests := make(chan svc.ChangeRequest) + responses := make(chan svc.Status) + + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + handler.Execute(nil, requests, responses) + wg.Done() + }() + + go func() { + resp := <-responses + assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately") + resp = <-responses + assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending") + assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown") + time.Sleep(time.Second) // let it run for a second + cancel() + resp = <-responses + assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after agent goroutine stops") + wg.Done() + }() + + wg.Wait() +} diff --git a/agent/app/args/flag.go b/agent/app/args/flag.go index 56b92569b98..a1a078e78af 100644 --- a/agent/app/args/flag.go +++ b/agent/app/args/flag.go @@ -18,10 +18,11 @@ import "flag" const ( versionUsage = "Print the agent version information and exit" logLevelUsage = "Loglevel: [||||]" + ecsAttributesUsage = "Print the Agent's ECS Attributes based on its environment" acceptInsecureCertUsage = "Disable SSL certificate verification. We do not recommend setting this option" licenseUsage = "Print the LICENSE and NOTICE files and exit" blacholeEC2MetadataUsage = "Blackhole the EC2 Metadata requests. Setting this option can cause the ECS Agent to fail to work properly. We do not recommend setting this option" - ecsAttributesUsage = "Print the Agent's ECS Attributes based on its environment" + windowsServiceUsage = "Run the ECS agent as a Windows Service" versionFlagName = "version" logLevelFlagName = "loglevel" @@ -29,6 +30,7 @@ const ( acceptInsecureCertFlagName = "k" licenseFlagName = "license" blackholeEC2MetadataFlagName = "blackhole-ec2-metadata" + windowsServiceFlagName = "windows-service" ) // Args wraps various ECS Agent arguments @@ -47,6 +49,8 @@ type Args struct { BlackholeEC2Metadata *bool // ECSAttributes indicates that the agent should print its attributes ECSAttributes *bool + // WindowsService indicates that the agent should run as a Windows service + WindowsService *bool } // New creates a new Args object from the argument list @@ -60,6 +64,7 @@ func New(arguments []string) (*Args, error) { License: flagset.Bool(licenseFlagName, false, licenseUsage), BlackholeEC2Metadata: flagset.Bool(blackholeEC2MetadataFlagName, false, blacholeEC2MetadataUsage), ECSAttributes: flagset.Bool(ecsAttributesFlagName, false, ecsAttributesUsage), + WindowsService: flagset.Bool(windowsServiceFlagName, false, windowsServiceUsage), } err := flagset.Parse(arguments) diff --git a/agent/app/run.go b/agent/app/run.go index 487ed630f6e..944d7054540 100644 --- a/agent/app/run.go +++ b/agent/app/run.go @@ -56,6 +56,9 @@ func Run(arguments []string) int { case *parsedArgs.ECSAttributes: // Print agent's ecs attributes based on its environment and exit return agent.printECSAttributes() + case *parsedArgs.WindowsService: + // Enable Windows Service + return agent.startWindowsService() default: // Start the agent return agent.start() diff --git a/agent/logger/eventlog_windows.go b/agent/logger/eventlog_windows.go new file mode 100644 index 00000000000..a2057a768a2 --- /dev/null +++ b/agent/logger/eventlog_windows.go @@ -0,0 +1,84 @@ +// +build windows + +// Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package logger + +import ( + "github.com/cihub/seelog" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/eventlog" +) + +/* +TODO: Make this whole thing better + +What's here right now is a stub just so that agent logs can appear in the Event Log. Longer term, we should do a few +things to make this better: + +1) Don't use init() and a package-global variable +2) Conform to the MSDN guidelines about event log data. + +References to MSDN guidelines can be found here: +* https://msdn.microsoft.com/en-us/library/windows/desktop/aa363632(v=vs.85).aspx +* https://msdn.microsoft.com/en-us/library/windows/desktop/aa363648(v=vs.85).aspx +* https://msdn.microsoft.com/en-us/library/windows/desktop/aa363661(v=vs.85).aspx +* https://msdn.microsoft.com/en-us/library/windows/desktop/aa363669(v=vs.85).aspx +*/ + +const ( + eventLogName = "AmazonECSAgent" + eventLogID = 999 +) + +// eventLogReceiver fulfills the seelog.CustomReceiver interface +type eventLogReceiver struct{} + +var eventLog *eventlog.Log + +func init() { + eventlog.InstallAsEventCreate(eventLogName, windows.EVENTLOG_INFORMATION_TYPE|windows.EVENTLOG_WARNING_TYPE|windows.EVENTLOG_ERROR_TYPE) + var err error + eventLog, err = eventlog.Open(eventLogName) + if err != nil { + panic(err) + } +} + +// registerPlatformLogger registers the eventLogReceiver +func registerPlatformLogger() { + seelog.RegisterReceiver("wineventlog", &eventLogReceiver{}) +} + +// platformLogConfig exposes log configuration for the event log receiver +func platformLogConfig() string { + return `` +} + +// ReceiveMessage receives a log line from seelog and emits it to the Windows event log +func (r *eventLogReceiver) ReceiveMessage(message string, level seelog.LogLevel, context seelog.LogContextInterface) error { + switch level { + case seelog.DebugLvl, seelog.InfoLvl: + return eventLog.Info(eventLogID, message) + case seelog.WarnLvl: + return eventLog.Warning(eventLogID, message) + case seelog.ErrorLvl, seelog.CriticalLvl: + return eventLog.Error(eventLogID, message) + } + return nil +} + +func (r *eventLogReceiver) AfterParse(initArgs seelog.CustomReceiverInitArgs) error { return nil } +func (r *eventLogReceiver) Flush() {} +func (r *eventLogReceiver) Close() error { return nil } diff --git a/agent/logger/log.go b/agent/logger/log.go index 9039442b5c4..0a1867c5cf8 100644 --- a/agent/logger/log.go +++ b/agent/logger/log.go @@ -59,6 +59,7 @@ func initLogger() { logfile = os.Getenv(LOGFILE_ENV_VAR) SetLevel(envLevel) + registerPlatformLogger() reloadConfig() } @@ -86,7 +87,7 @@ func SetLevel(logLevel string) { // GetLevel gets the log level func GetLevel() string { levelLock.RLock() - defer levelLock.RLock() + defer levelLock.RUnlock() return level } diff --git a/agent/logger/platform_unix.go b/agent/logger/platform_unix.go new file mode 100644 index 00000000000..cab8d236f5d --- /dev/null +++ b/agent/logger/platform_unix.go @@ -0,0 +1,22 @@ +// +build !windows + +// Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package logger + +// registerPlatformLogger does nothing on Linux +func registerPlatformLogger() {} + +// platformLogConfig does nothing on Linux +func platformLogConfig() string { return "" } diff --git a/agent/logger/seelog_config.go b/agent/logger/seelog_config.go index 43b87b62703..3211a115cc1 100644 --- a/agent/logger/seelog_config.go +++ b/agent/logger/seelog_config.go @@ -18,6 +18,7 @@ func loggerConfig() string { ` + config += platformLogConfig() if logfile != "" { config += `` @@ -26,6 +27,7 @@ func loggerConfig() string { + ` diff --git a/agent/sighandlers/termination_handler.go b/agent/sighandlers/termination_handler.go index c2b4afa2efb..b7a5f34863c 100644 --- a/agent/sighandlers/termination_handler.go +++ b/agent/sighandlers/termination_handler.go @@ -11,7 +11,7 @@ // express or implied. See the License for the specific language governing // permissions and limitations under the License. -// sighandlers handle signals and behave appropriately. +// Package sighandlers handle signals and behave appropriately. // SIGTERM: // Flush state to disk and exit // SIGUSR1: @@ -26,33 +26,38 @@ import ( "time" "github.com/aws/amazon-ecs-agent/agent/engine" - "github.com/aws/amazon-ecs-agent/agent/logger" "github.com/aws/amazon-ecs-agent/agent/sighandlers/exitcodes" "github.com/aws/amazon-ecs-agent/agent/statemanager" "github.com/aws/amazon-ecs-agent/agent/utils" + + "github.com/cihub/seelog" +) + +const ( + engineDisableTimeout = 5 * time.Second + finalSaveTimeout = 3 * time.Second ) -var log = logger.ForModule("TerminationHandler") +// TerminationHandler defines a handler used for terminating the agent +type TerminationHandler func(saver statemanager.Saver, taskEngine engine.TaskEngine) -func StartTerminationHandler(saver statemanager.Saver, taskEngine engine.TaskEngine) { +// StartDefaultTerminationHandler defines a default termination handler suitable for running in a process +func StartDefaultTerminationHandler(saver statemanager.Saver, taskEngine engine.TaskEngine) { signalChannel := make(chan os.Signal, 2) signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) sig := <-signalChannel - log.Debug("Received termination signal", "signal", sig.String()) + seelog.Debugf("Termination handler received termination signal: %s", sig.String()) err := FinalSave(saver, taskEngine) if err != nil { - log.Crit("Error saving state before final shutdown", "err", err) + seelog.Criticalf("Error saving state before final shutdown: %v", err) // Terminal because it's a sigterm; the user doesn't want it to restart os.Exit(exitcodes.ExitTerminal) } os.Exit(exitcodes.ExitSuccess) } -const engineDisableTimeout = 5 * time.Second -const finalSaveTimeout = 3 * time.Second - // FinalSave should be called immediately before exiting, and only before // exiting, in order to flush tasks to disk. It waits a short timeout for state // to settle if necessary. If unable to reach a steady-state and save within @@ -61,11 +66,11 @@ func FinalSave(saver statemanager.Saver, taskEngine engine.TaskEngine) error { engineDisabled := make(chan error) disableTimer := time.AfterFunc(engineDisableTimeout, func() { - engineDisabled <- errors.New("Timed out waiting for TaskEngine to settle") + engineDisabled <- errors.New("final save: timed out waiting for TaskEngine to settle") }) go func() { - log.Debug("Shutting down task engine") + seelog.Debug("Shutting down task engine for final save") taskEngine.Disable() disableTimer.Stop() engineDisabled <- nil @@ -75,10 +80,10 @@ func FinalSave(saver statemanager.Saver, taskEngine engine.TaskEngine) error { stateSaved := make(chan error) saveTimer := time.AfterFunc(finalSaveTimeout, func() { - stateSaved <- errors.New("Timed out trying to save to disk") + stateSaved <- errors.New("final save: timed out trying to save to disk") }) go func() { - log.Debug("Saving state before shutting down") + seelog.Debug("Saving state before shutting down") stateSaved <- saver.ForceSave() saveTimer.Stop() }() diff --git a/agent/vendor/golang.org/x/sys/windows/svc/event.go b/agent/vendor/golang.org/x/sys/windows/svc/event.go new file mode 100644 index 00000000000..0508e228818 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/event.go @@ -0,0 +1,48 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package svc + +import ( + "errors" + + "golang.org/x/sys/windows" +) + +// event represents auto-reset, initially non-signaled Windows event. +// It is used to communicate between go and asm parts of this package. +type event struct { + h windows.Handle +} + +func newEvent() (*event, error) { + h, err := windows.CreateEvent(nil, 0, 0, nil) + if err != nil { + return nil, err + } + return &event{h: h}, nil +} + +func (e *event) Close() error { + return windows.CloseHandle(e.h) +} + +func (e *event) Set() error { + return windows.SetEvent(e.h) +} + +func (e *event) Wait() error { + s, err := windows.WaitForSingleObject(e.h, windows.INFINITE) + switch s { + case windows.WAIT_OBJECT_0: + break + case windows.WAIT_FAILED: + return err + default: + return errors.New("unexpected result from WaitForSingleObject") + } + return nil +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/eventlog/install.go b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/install.go new file mode 100644 index 00000000000..c76a3760a42 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/install.go @@ -0,0 +1,80 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package eventlog + +import ( + "errors" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" +) + +const ( + // Log levels. + Info = windows.EVENTLOG_INFORMATION_TYPE + Warning = windows.EVENTLOG_WARNING_TYPE + Error = windows.EVENTLOG_ERROR_TYPE +) + +const addKeyName = `SYSTEM\CurrentControlSet\Services\EventLog\Application` + +// Install modifies PC registry to allow logging with an event source src. +// It adds all required keys and values to the event log registry key. +// Install uses msgFile as the event message file. If useExpandKey is true, +// the event message file is installed as REG_EXPAND_SZ value, +// otherwise as REG_SZ. Use bitwise of log.Error, log.Warning and +// log.Info to specify events supported by the new event source. +func Install(src, msgFile string, useExpandKey bool, eventsSupported uint32) error { + appkey, err := registry.OpenKey(registry.LOCAL_MACHINE, addKeyName, registry.CREATE_SUB_KEY) + if err != nil { + return err + } + defer appkey.Close() + + sk, alreadyExist, err := registry.CreateKey(appkey, src, registry.SET_VALUE) + if err != nil { + return err + } + defer sk.Close() + if alreadyExist { + return errors.New(addKeyName + `\` + src + " registry key already exists") + } + + err = sk.SetDWordValue("CustomSource", 1) + if err != nil { + return err + } + if useExpandKey { + err = sk.SetExpandStringValue("EventMessageFile", msgFile) + } else { + err = sk.SetStringValue("EventMessageFile", msgFile) + } + if err != nil { + return err + } + err = sk.SetDWordValue("TypesSupported", eventsSupported) + if err != nil { + return err + } + return nil +} + +// InstallAsEventCreate is the same as Install, but uses +// %SystemRoot%\System32\EventCreate.exe as the event message file. +func InstallAsEventCreate(src string, eventsSupported uint32) error { + return Install(src, "%SystemRoot%\\System32\\EventCreate.exe", true, eventsSupported) +} + +// Remove deletes all registry elements installed by the correspondent Install. +func Remove(src string) error { + appkey, err := registry.OpenKey(registry.LOCAL_MACHINE, addKeyName, registry.SET_VALUE) + if err != nil { + return err + } + defer appkey.Close() + return registry.DeleteKey(appkey, src) +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log.go b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log.go new file mode 100644 index 00000000000..46e5153d024 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log.go @@ -0,0 +1,70 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// Package eventlog implements access to Windows event log. +// +package eventlog + +import ( + "errors" + "syscall" + + "golang.org/x/sys/windows" +) + +// Log provides access to the system log. +type Log struct { + Handle windows.Handle +} + +// Open retrieves a handle to the specified event log. +func Open(source string) (*Log, error) { + return OpenRemote("", source) +} + +// OpenRemote does the same as Open, but on different computer host. +func OpenRemote(host, source string) (*Log, error) { + if source == "" { + return nil, errors.New("Specify event log source") + } + var s *uint16 + if host != "" { + s = syscall.StringToUTF16Ptr(host) + } + h, err := windows.RegisterEventSource(s, syscall.StringToUTF16Ptr(source)) + if err != nil { + return nil, err + } + return &Log{Handle: h}, nil +} + +// Close closes event log l. +func (l *Log) Close() error { + return windows.DeregisterEventSource(l.Handle) +} + +func (l *Log) report(etype uint16, eid uint32, msg string) error { + ss := []*uint16{syscall.StringToUTF16Ptr(msg)} + return windows.ReportEvent(l.Handle, etype, 0, eid, 0, 1, 0, &ss[0], nil) +} + +// Info writes an information event msg with event id eid to the end of event log l. +// When EventCreate.exe is used, eid must be between 1 and 1000. +func (l *Log) Info(eid uint32, msg string) error { + return l.report(windows.EVENTLOG_INFORMATION_TYPE, eid, msg) +} + +// Warning writes an warning event msg with event id eid to the end of event log l. +// When EventCreate.exe is used, eid must be between 1 and 1000. +func (l *Log) Warning(eid uint32, msg string) error { + return l.report(windows.EVENTLOG_WARNING_TYPE, eid, msg) +} + +// Error writes an error event msg with event id eid to the end of event log l. +// When EventCreate.exe is used, eid must be between 1 and 1000. +func (l *Log) Error(eid uint32, msg string) error { + return l.report(windows.EVENTLOG_ERROR_TYPE, eid, msg) +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log_test.go b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log_test.go new file mode 100644 index 00000000000..4dd8ad9e746 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/eventlog/log_test.go @@ -0,0 +1,51 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package eventlog_test + +import ( + "testing" + + "golang.org/x/sys/windows/svc/eventlog" +) + +func TestLog(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode - it modifies system logs") + } + + const name = "mylog" + const supports = eventlog.Error | eventlog.Warning | eventlog.Info + err := eventlog.InstallAsEventCreate(name, supports) + if err != nil { + t.Fatalf("Install failed: %s", err) + } + defer func() { + err = eventlog.Remove(name) + if err != nil { + t.Fatalf("Remove failed: %s", err) + } + }() + + l, err := eventlog.Open(name) + if err != nil { + t.Fatalf("Open failed: %s", err) + } + defer l.Close() + + err = l.Info(1, "info") + if err != nil { + t.Fatalf("Info failed: %s", err) + } + err = l.Warning(2, "warning") + if err != nil { + t.Fatalf("Warning failed: %s", err) + } + err = l.Error(3, "error") + if err != nil { + t.Fatalf("Error failed: %s", err) + } +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/go12.c b/agent/vendor/golang.org/x/sys/windows/svc/go12.c new file mode 100644 index 00000000000..6f1be1fa3bc --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/go12.c @@ -0,0 +1,24 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows +// +build !go1.3 + +// copied from pkg/runtime +typedef unsigned int uint32; +typedef unsigned long long int uint64; +#ifdef _64BIT +typedef uint64 uintptr; +#else +typedef uint32 uintptr; +#endif + +// from sys_386.s or sys_amd64.s +void ·servicemain(void); + +void +·getServiceMain(uintptr *r) +{ + *r = (uintptr)·servicemain; +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/go12.go b/agent/vendor/golang.org/x/sys/windows/svc/go12.go new file mode 100644 index 00000000000..6f0a924eafd --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/go12.go @@ -0,0 +1,11 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows +// +build !go1.3 + +package svc + +// from go12.c +func getServiceMain(r *uintptr) diff --git a/agent/vendor/golang.org/x/sys/windows/svc/go13.go b/agent/vendor/golang.org/x/sys/windows/svc/go13.go new file mode 100644 index 00000000000..432a9e796a7 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/go13.go @@ -0,0 +1,31 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows +// +build go1.3 + +package svc + +import "unsafe" + +const ptrSize = 4 << (^uintptr(0) >> 63) // unsafe.Sizeof(uintptr(0)) but an ideal const + +// Should be a built-in for unsafe.Pointer? +func add(p unsafe.Pointer, x uintptr) unsafe.Pointer { + return unsafe.Pointer(uintptr(p) + x) +} + +// funcPC returns the entry PC of the function f. +// It assumes that f is a func value. Otherwise the behavior is undefined. +func funcPC(f interface{}) uintptr { + return **(**uintptr)(add(unsafe.Pointer(&f), ptrSize)) +} + +// from sys_386.s and sys_amd64.s +func servicectlhandler(ctl uint32) uintptr +func servicemain(argc uint32, argv **uint16) + +func getServiceMain(r *uintptr) { + *r = funcPC(servicemain) +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/security.go b/agent/vendor/golang.org/x/sys/windows/svc/security.go new file mode 100644 index 00000000000..6fbc9236ed5 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/security.go @@ -0,0 +1,62 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package svc + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +func allocSid(subAuth0 uint32) (*windows.SID, error) { + var sid *windows.SID + err := windows.AllocateAndInitializeSid(&windows.SECURITY_NT_AUTHORITY, + 1, subAuth0, 0, 0, 0, 0, 0, 0, 0, &sid) + if err != nil { + return nil, err + } + return sid, nil +} + +// IsAnInteractiveSession determines if calling process is running interactively. +// It queries the process token for membership in the Interactive group. +// http://stackoverflow.com/questions/2668851/how-do-i-detect-that-my-application-is-running-as-service-or-in-an-interactive-s +func IsAnInteractiveSession() (bool, error) { + interSid, err := allocSid(windows.SECURITY_INTERACTIVE_RID) + if err != nil { + return false, err + } + defer windows.FreeSid(interSid) + + serviceSid, err := allocSid(windows.SECURITY_SERVICE_RID) + if err != nil { + return false, err + } + defer windows.FreeSid(serviceSid) + + t, err := windows.OpenCurrentProcessToken() + if err != nil { + return false, err + } + defer t.Close() + + gs, err := t.GetTokenGroups() + if err != nil { + return false, err + } + p := unsafe.Pointer(&gs.Groups[0]) + groups := (*[2 << 20]windows.SIDAndAttributes)(p)[:gs.GroupCount] + for _, g := range groups { + if windows.EqualSid(g.Sid, interSid) { + return true, nil + } + if windows.EqualSid(g.Sid, serviceSid) { + return false, nil + } + } + return false, nil +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/service.go b/agent/vendor/golang.org/x/sys/windows/svc/service.go new file mode 100644 index 00000000000..9864f7a72f2 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/service.go @@ -0,0 +1,316 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// Package svc provides everything required to build Windows service. +// +package svc + +import ( + "errors" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// State describes service execution state (Stopped, Running and so on). +type State uint32 + +const ( + Stopped = State(windows.SERVICE_STOPPED) + StartPending = State(windows.SERVICE_START_PENDING) + StopPending = State(windows.SERVICE_STOP_PENDING) + Running = State(windows.SERVICE_RUNNING) + ContinuePending = State(windows.SERVICE_CONTINUE_PENDING) + PausePending = State(windows.SERVICE_PAUSE_PENDING) + Paused = State(windows.SERVICE_PAUSED) +) + +// Cmd represents service state change request. It is sent to a service +// by the service manager, and should be actioned upon by the service. +type Cmd uint32 + +const ( + Stop = Cmd(windows.SERVICE_CONTROL_STOP) + Pause = Cmd(windows.SERVICE_CONTROL_PAUSE) + Continue = Cmd(windows.SERVICE_CONTROL_CONTINUE) + Interrogate = Cmd(windows.SERVICE_CONTROL_INTERROGATE) + Shutdown = Cmd(windows.SERVICE_CONTROL_SHUTDOWN) +) + +// Accepted is used to describe commands accepted by the service. +// Note that Interrogate is always accepted. +type Accepted uint32 + +const ( + AcceptStop = Accepted(windows.SERVICE_ACCEPT_STOP) + AcceptShutdown = Accepted(windows.SERVICE_ACCEPT_SHUTDOWN) + AcceptPauseAndContinue = Accepted(windows.SERVICE_ACCEPT_PAUSE_CONTINUE) +) + +// Status combines State and Accepted commands to fully describe running service. +type Status struct { + State State + Accepts Accepted + CheckPoint uint32 // used to report progress during a lengthy operation + WaitHint uint32 // estimated time required for a pending operation, in milliseconds +} + +// ChangeRequest is sent to the service Handler to request service status change. +type ChangeRequest struct { + Cmd Cmd + CurrentStatus Status +} + +// Handler is the interface that must be implemented to build Windows service. +type Handler interface { + + // Execute will be called by the package code at the start of + // the service, and the service will exit once Execute completes. + // Inside Execute you must read service change requests from r and + // act accordingly. You must keep service control manager up to date + // about state of your service by writing into s as required. + // args contains service name followed by argument strings passed + // to the service. + // You can provide service exit code in exitCode return parameter, + // with 0 being "no error". You can also indicate if exit code, + // if any, is service specific or not by using svcSpecificEC + // parameter. + Execute(args []string, r <-chan ChangeRequest, s chan<- Status) (svcSpecificEC bool, exitCode uint32) +} + +var ( + // These are used by asm code. + goWaitsH uintptr + cWaitsH uintptr + ssHandle uintptr + sName *uint16 + sArgc uintptr + sArgv **uint16 + ctlHandlerProc uintptr + cSetEvent uintptr + cWaitForSingleObject uintptr + cRegisterServiceCtrlHandlerW uintptr +) + +func init() { + k := syscall.MustLoadDLL("kernel32.dll") + cSetEvent = k.MustFindProc("SetEvent").Addr() + cWaitForSingleObject = k.MustFindProc("WaitForSingleObject").Addr() + a := syscall.MustLoadDLL("advapi32.dll") + cRegisterServiceCtrlHandlerW = a.MustFindProc("RegisterServiceCtrlHandlerW").Addr() +} + +type ctlEvent struct { + cmd Cmd + errno uint32 +} + +// service provides access to windows service api. +type service struct { + name string + h windows.Handle + cWaits *event + goWaits *event + c chan ctlEvent + handler Handler +} + +func newService(name string, handler Handler) (*service, error) { + var s service + var err error + s.name = name + s.c = make(chan ctlEvent) + s.handler = handler + s.cWaits, err = newEvent() + if err != nil { + return nil, err + } + s.goWaits, err = newEvent() + if err != nil { + s.cWaits.Close() + return nil, err + } + return &s, nil +} + +func (s *service) close() error { + s.cWaits.Close() + s.goWaits.Close() + return nil +} + +type exitCode struct { + isSvcSpecific bool + errno uint32 +} + +func (s *service) updateStatus(status *Status, ec *exitCode) error { + if s.h == 0 { + return errors.New("updateStatus with no service status handle") + } + var t windows.SERVICE_STATUS + t.ServiceType = windows.SERVICE_WIN32_OWN_PROCESS + t.CurrentState = uint32(status.State) + if status.Accepts&AcceptStop != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_STOP + } + if status.Accepts&AcceptShutdown != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_SHUTDOWN + } + if status.Accepts&AcceptPauseAndContinue != 0 { + t.ControlsAccepted |= windows.SERVICE_ACCEPT_PAUSE_CONTINUE + } + if ec.errno == 0 { + t.Win32ExitCode = windows.NO_ERROR + t.ServiceSpecificExitCode = windows.NO_ERROR + } else if ec.isSvcSpecific { + t.Win32ExitCode = uint32(windows.ERROR_SERVICE_SPECIFIC_ERROR) + t.ServiceSpecificExitCode = ec.errno + } else { + t.Win32ExitCode = ec.errno + t.ServiceSpecificExitCode = windows.NO_ERROR + } + t.CheckPoint = status.CheckPoint + t.WaitHint = status.WaitHint + return windows.SetServiceStatus(s.h, &t) +} + +const ( + sysErrSetServiceStatusFailed = uint32(syscall.APPLICATION_ERROR) + iota + sysErrNewThreadInCallback +) + +func (s *service) run() { + s.goWaits.Wait() + s.h = windows.Handle(ssHandle) + argv := (*[100]*int16)(unsafe.Pointer(sArgv))[:sArgc] + args := make([]string, len(argv)) + for i, a := range argv { + args[i] = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(a))[:]) + } + + cmdsToHandler := make(chan ChangeRequest) + changesFromHandler := make(chan Status) + exitFromHandler := make(chan exitCode) + + go func() { + ss, errno := s.handler.Execute(args, cmdsToHandler, changesFromHandler) + exitFromHandler <- exitCode{ss, errno} + }() + + status := Status{State: Stopped} + ec := exitCode{isSvcSpecific: true, errno: 0} + var outch chan ChangeRequest + inch := s.c + var cmd Cmd +loop: + for { + select { + case r := <-inch: + if r.errno != 0 { + ec.errno = r.errno + break loop + } + inch = nil + outch = cmdsToHandler + cmd = r.cmd + case outch <- ChangeRequest{cmd, status}: + inch = s.c + outch = nil + case c := <-changesFromHandler: + err := s.updateStatus(&c, &ec) + if err != nil { + // best suitable error number + ec.errno = sysErrSetServiceStatusFailed + if err2, ok := err.(syscall.Errno); ok { + ec.errno = uint32(err2) + } + break loop + } + status = c + case ec = <-exitFromHandler: + break loop + } + } + + s.updateStatus(&Status{State: Stopped}, &ec) + s.cWaits.Set() +} + +func newCallback(fn interface{}) (cb uintptr, err error) { + defer func() { + r := recover() + if r == nil { + return + } + cb = 0 + switch v := r.(type) { + case string: + err = errors.New(v) + case error: + err = v + default: + err = errors.New("unexpected panic in syscall.NewCallback") + } + }() + return syscall.NewCallback(fn), nil +} + +// BUG(brainman): There is no mechanism to run multiple services +// inside one single executable. Perhaps, it can be overcome by +// using RegisterServiceCtrlHandlerEx Windows api. + +// Run executes service name by calling appropriate handler function. +func Run(name string, handler Handler) error { + runtime.LockOSThread() + + tid := windows.GetCurrentThreadId() + + s, err := newService(name, handler) + if err != nil { + return err + } + + ctlHandler := func(ctl uint32) uintptr { + e := ctlEvent{cmd: Cmd(ctl)} + // We assume that this callback function is running on + // the same thread as Run. Nowhere in MS documentation + // I could find statement to guarantee that. So putting + // check here to verify, otherwise things will go bad + // quickly, if ignored. + i := windows.GetCurrentThreadId() + if i != tid { + e.errno = sysErrNewThreadInCallback + } + s.c <- e + return 0 + } + + var svcmain uintptr + getServiceMain(&svcmain) + t := []windows.SERVICE_TABLE_ENTRY{ + {syscall.StringToUTF16Ptr(s.name), svcmain}, + {nil, 0}, + } + + goWaitsH = uintptr(s.goWaits.h) + cWaitsH = uintptr(s.cWaits.h) + sName = t[0].ServiceName + ctlHandlerProc, err = newCallback(ctlHandler) + if err != nil { + return err + } + + go s.run() + + err = windows.StartServiceCtrlDispatcher(&t[0]) + if err != nil { + return err + } + return nil +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/svc_test.go b/agent/vendor/golang.org/x/sys/windows/svc/svc_test.go new file mode 100644 index 00000000000..764da54a547 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/svc_test.go @@ -0,0 +1,118 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package svc_test + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" +) + +func getState(t *testing.T, s *mgr.Service) svc.State { + status, err := s.Query() + if err != nil { + t.Fatalf("Query(%s) failed: %s", s.Name, err) + } + return status.State +} + +func testState(t *testing.T, s *mgr.Service, want svc.State) { + have := getState(t, s) + if have != want { + t.Fatalf("%s state is=%d want=%d", s.Name, have, want) + } +} + +func waitState(t *testing.T, s *mgr.Service, want svc.State) { + for i := 0; ; i++ { + have := getState(t, s) + if have == want { + return + } + if i > 10 { + t.Fatalf("%s state is=%d, waiting timeout", s.Name, have) + } + time.Sleep(300 * time.Millisecond) + } +} + +func TestExample(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode - it modifies system services") + } + + const name = "myservice" + + m, err := mgr.Connect() + if err != nil { + t.Fatalf("SCM connection failed: %s", err) + } + defer m.Disconnect() + + dir, err := ioutil.TempDir("", "svc") + if err != nil { + t.Fatalf("failed to create temp directory: %v", err) + } + defer os.RemoveAll(dir) + + exepath := filepath.Join(dir, "a.exe") + o, err := exec.Command("go", "build", "-o", exepath, "golang.org/x/sys/windows/svc/example").CombinedOutput() + if err != nil { + t.Fatalf("failed to build service program: %v\n%v", err, string(o)) + } + + s, err := m.OpenService(name) + if err == nil { + err = s.Delete() + if err != nil { + s.Close() + t.Fatalf("Delete failed: %s", err) + } + s.Close() + } + s, err = m.CreateService(name, exepath, mgr.Config{DisplayName: "my service"}, "is", "auto-started") + if err != nil { + t.Fatalf("CreateService(%s) failed: %v", name, err) + } + defer s.Close() + + testState(t, s, svc.Stopped) + err = s.Start("is", "manual-started") + if err != nil { + t.Fatalf("Start(%s) failed: %s", s.Name, err) + } + waitState(t, s, svc.Running) + time.Sleep(1 * time.Second) + + // testing deadlock from issues 4. + _, err = s.Control(svc.Interrogate) + if err != nil { + t.Fatalf("Control(%s) failed: %s", s.Name, err) + } + _, err = s.Control(svc.Interrogate) + if err != nil { + t.Fatalf("Control(%s) failed: %s", s.Name, err) + } + time.Sleep(1 * time.Second) + + _, err = s.Control(svc.Stop) + if err != nil { + t.Fatalf("Control(%s) failed: %s", s.Name, err) + } + waitState(t, s, svc.Stopped) + + err = s.Delete() + if err != nil { + t.Fatalf("Delete failed: %s", err) + } +} diff --git a/agent/vendor/golang.org/x/sys/windows/svc/sys_386.s b/agent/vendor/golang.org/x/sys/windows/svc/sys_386.s new file mode 100644 index 00000000000..5e11bfadb52 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/sys_386.s @@ -0,0 +1,67 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// func servicemain(argc uint32, argv **uint16) +TEXT ·servicemain(SB),7,$0 + MOVL argc+0(FP), AX + MOVL AX, ·sArgc(SB) + MOVL argv+4(FP), AX + MOVL AX, ·sArgv(SB) + + PUSHL BP + PUSHL BX + PUSHL SI + PUSHL DI + + SUBL $12, SP + + MOVL ·sName(SB), AX + MOVL AX, (SP) + MOVL $·servicectlhandler(SB), AX + MOVL AX, 4(SP) + MOVL ·cRegisterServiceCtrlHandlerW(SB), AX + MOVL SP, BP + CALL AX + MOVL BP, SP + CMPL AX, $0 + JE exit + MOVL AX, ·ssHandle(SB) + + MOVL ·goWaitsH(SB), AX + MOVL AX, (SP) + MOVL ·cSetEvent(SB), AX + MOVL SP, BP + CALL AX + MOVL BP, SP + + MOVL ·cWaitsH(SB), AX + MOVL AX, (SP) + MOVL $-1, AX + MOVL AX, 4(SP) + MOVL ·cWaitForSingleObject(SB), AX + MOVL SP, BP + CALL AX + MOVL BP, SP + +exit: + ADDL $12, SP + + POPL DI + POPL SI + POPL BX + POPL BP + + MOVL 0(SP), CX + ADDL $12, SP + JMP CX + +// I do not know why, but this seems to be the only way to call +// ctlHandlerProc on Windows 7. + +// func servicectlhandler(ctl uint32) uintptr +TEXT ·servicectlhandler(SB),7,$0 + MOVL ·ctlHandlerProc(SB), CX + JMP CX diff --git a/agent/vendor/golang.org/x/sys/windows/svc/sys_amd64.s b/agent/vendor/golang.org/x/sys/windows/svc/sys_amd64.s new file mode 100644 index 00000000000..87dbec83952 --- /dev/null +++ b/agent/vendor/golang.org/x/sys/windows/svc/sys_amd64.s @@ -0,0 +1,41 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +// func servicemain(argc uint32, argv **uint16) +TEXT ·servicemain(SB),7,$0 + MOVL CX, ·sArgc(SB) + MOVL DX, ·sArgv(SB) + + SUBQ $32, SP // stack for the first 4 syscall params + + MOVQ ·sName(SB), CX + MOVQ $·servicectlhandler(SB), DX + MOVQ ·cRegisterServiceCtrlHandlerW(SB), AX + CALL AX + CMPQ AX, $0 + JE exit + MOVQ AX, ·ssHandle(SB) + + MOVQ ·goWaitsH(SB), CX + MOVQ ·cSetEvent(SB), AX + CALL AX + + MOVQ ·cWaitsH(SB), CX + MOVQ $4294967295, DX + MOVQ ·cWaitForSingleObject(SB), AX + CALL AX + +exit: + ADDQ $32, SP + RET + +// I do not know why, but this seems to be the only way to call +// ctlHandlerProc on Windows 7. + +// func servicectlhandler(ctl uint32) uintptr +TEXT ·servicectlhandler(SB),7,$0 + MOVQ ·ctlHandlerProc(SB), AX + JMP AX