-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Add more detailed errors and tests in the windows/service metricset #17725
Changes from all commits
28331d3
cffde58
84c1dca
97bcb45
70f54e9
f28f5c4
4a139ab
3fe1576
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 |
---|---|---|
@@ -0,0 +1,197 @@ | ||
// Licensed to Elasticsearch B.V. under one or more contributor | ||
// license agreements. See the NOTICE file distributed with | ||
// this work for additional information regarding copyright | ||
// ownership. Elasticsearch B.V. licenses this file to you under | ||
// the Apache License, Version 2.0 (the "License"); you may | ||
// not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the License 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. | ||
|
||
// +build windows | ||
|
||
package service | ||
|
||
import ( | ||
"crypto/sha256" | ||
"encoding/base64" | ||
"strconv" | ||
"syscall" | ||
|
||
"github.com/pkg/errors" | ||
"golang.org/x/sys/windows/registry" | ||
|
||
"github.com/elastic/beats/v7/libbeat/common" | ||
) | ||
|
||
var ( | ||
// errorNames is mapping of errno values to names. | ||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681383(v=vs.85).aspx | ||
errorNames = map[uint32]string{ | ||
1077: "ERROR_SERVICE_NEVER_STARTED", | ||
} | ||
InvalidDatabaseHandle = ^Handle(0) | ||
) | ||
|
||
type Handle uintptr | ||
|
||
type Reader struct { | ||
handle Handle | ||
state ServiceEnumState | ||
guid string // Host's MachineGuid value (a unique ID for the host). | ||
ids map[string]string // Cache of service IDs. | ||
protectedServices map[string]struct{} | ||
} | ||
|
||
func NewReader() (*Reader, error) { | ||
handle, err := openSCManager("", "", ScManagerEnumerateService|ScManagerConnect) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "initialization failed") | ||
} | ||
|
||
guid, err := getMachineGUID() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
r := &Reader{ | ||
handle: handle, | ||
state: ServiceStateAll, | ||
guid: guid, | ||
ids: map[string]string{}, | ||
protectedServices: map[string]struct{}{}, | ||
} | ||
|
||
return r, nil | ||
} | ||
|
||
func (reader *Reader) Read() ([]common.MapStr, error) { | ||
services, err := GetServiceStates(reader.handle, reader.state, reader.protectedServices) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
result := make([]common.MapStr, 0, len(services)) | ||
|
||
for _, service := range services { | ||
ev := common.MapStr{ | ||
"id": reader.getServiceID(service.ServiceName), | ||
"display_name": service.DisplayName, | ||
"name": service.ServiceName, | ||
"state": service.CurrentState, | ||
"start_type": service.StartType.String(), | ||
"start_name": service.ServiceStartName, | ||
"path_name": service.BinaryPathName, | ||
} | ||
|
||
if service.CurrentState == "Stopped" { | ||
ev.Put("exit_code", getErrorCode(service.ExitCode)) | ||
} | ||
|
||
if service.PID > 0 { | ||
ev.Put("pid", service.PID) | ||
} | ||
|
||
if service.Uptime > 0 { | ||
if _, err = ev.Put("uptime.ms", service.Uptime); err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
result = append(result, ev) | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
func (reader *Reader) Close() error { | ||
return closeHandle(reader.handle) | ||
} | ||
|
||
func openSCManager(machineName string, databaseName string, desiredAccess ServiceSCMAccessRight) (Handle, error) { | ||
var machineNamePtr *uint16 | ||
if machineName != "" { | ||
var err error | ||
machineNamePtr, err = syscall.UTF16PtrFromString(machineName) | ||
if err != nil { | ||
return InvalidDatabaseHandle, err | ||
} | ||
} | ||
|
||
var databaseNamePtr *uint16 | ||
if databaseName != "" { | ||
var err error | ||
databaseNamePtr, err = syscall.UTF16PtrFromString(databaseName) | ||
if err != nil { | ||
return InvalidDatabaseHandle, err | ||
} | ||
} | ||
|
||
handle, err := _OpenSCManager(machineNamePtr, databaseNamePtr, desiredAccess) | ||
if err != nil { | ||
return InvalidDatabaseHandle, ServiceErrno(err.(syscall.Errno)) | ||
} | ||
|
||
return handle, nil | ||
} | ||
|
||
// getMachineGUID returns the machine's GUID value which is unique to a Windows | ||
// installation. | ||
func getMachineGUID() (string, error) { | ||
const key = registry.LOCAL_MACHINE | ||
const path = `SOFTWARE\Microsoft\Cryptography` | ||
const name = "MachineGuid" | ||
|
||
k, err := registry.OpenKey(key, path, registry.READ|registry.WOW64_64KEY) | ||
if err != nil { | ||
return "", errors.Wrapf(err, `failed to open HKLM\%v`, path) | ||
} | ||
|
||
guid, _, err := k.GetStringValue(name) | ||
if err != nil { | ||
return "", errors.Wrapf(err, `failed to get value of HKLM\%v\%v`, path, name) | ||
} | ||
|
||
return guid, nil | ||
} | ||
|
||
// getServiceID returns a unique ID for the service that is derived from the | ||
// machine's GUID and the service's name. | ||
func (reader *Reader) getServiceID(name string) string { | ||
// hash returns a base64 encoded sha256 hash that is truncated to 10 chars. | ||
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. Why truncating? 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. Not sure, this seems to be a custom function that builds a unique service ID so users can distinct between services on different machines. I think the truncation is just to have a standard format for it. (just guessing here) |
||
hash := func(v string) string { | ||
sum := sha256.Sum256([]byte(v)) | ||
base64Hash := base64.RawURLEncoding.EncodeToString(sum[:]) | ||
return base64Hash[:10] | ||
} | ||
|
||
id, found := reader.ids[name] | ||
if !found { | ||
id = hash(reader.guid + name) | ||
reader.ids[name] = id | ||
} | ||
|
||
return id | ||
} | ||
|
||
func getErrorCode(errno uint32) string { | ||
name, found := errorNames[errno] | ||
if found { | ||
return name | ||
} | ||
return strconv.Itoa(int(errno)) | ||
} | ||
|
||
func closeHandle(handle Handle) error { | ||
if err := _CloseServiceHandle(uintptr(handle)); err != nil { | ||
return ServiceErrno(err.(syscall.Errno)) | ||
} | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// Licensed to Elasticsearch B.V. under one or more contributor | ||
// license agreements. See the NOTICE file distributed with | ||
// this work for additional information regarding copyright | ||
// ownership. Elasticsearch B.V. licenses this file to you under | ||
// the Apache License, Version 2.0 (the "License"); you may | ||
// not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the License 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. | ||
|
||
// +build windows | ||
|
||
package service | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestNewReader(t *testing.T) { | ||
reader, err := NewReader() | ||
assert.NoError(t, err) | ||
assert.NotNil(t, reader) | ||
defer reader.Close() | ||
assert.NotNil(t, reader.handle) | ||
} | ||
|
||
func TestOpenSCManager(t *testing.T) { | ||
handle, err := openSCManager("invalidMachine", "", ScManagerEnumerateService|ScManagerConnect) | ||
assert.Error(t, err) | ||
assert.Equal(t, handle, InvalidDatabaseHandle) | ||
|
||
handle, err = openSCManager("", "invalidDbName", ScManagerEnumerateService|ScManagerConnect) | ||
assert.Error(t, err) | ||
assert.Equal(t, handle, InvalidDatabaseHandle) | ||
|
||
handle, err = openSCManager("", "", ScManagerEnumerateService|ScManagerConnect) | ||
assert.NoError(t, err) | ||
assert.NotEqual(t, handle, InvalidDatabaseHandle) | ||
closeHandle(handle) | ||
} | ||
|
||
func TestGetMachineGUID(t *testing.T) { | ||
guid, err := getMachineGUID() | ||
assert.NoError(t, err) | ||
assert.NotNil(t, guid) | ||
} | ||
|
||
func TestRead(t *testing.T) { | ||
reader, err := NewReader() | ||
assert.NoError(t, err) | ||
result, err := reader.Read() | ||
assert.NoError(t, err) | ||
assert.True(t, len(result) > 0) | ||
reader.Close() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this error somehow special? Would we want to add more error names here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure why there is only one, this could be the most common one but I would either add several more or strip this entire logic all together and just retrieve the error code. I would go for the latter, what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me. As it comes from the existing code we can also leave this for a separated change.