-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Added BinPath, Description, UserName and Delayed Auto Start to Get-Service #4907
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
Conversation
…information to Get-Service command
| dwDesiredAccess: NativeMethods.SC_MANAGER_CONNECT | ||
| ); | ||
| if (IntPtr.Zero == hScManager) { | ||
| lastError = Marshal.GetLastWin32Error(); |
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.
Seems like by the time this is called, we know service is valid so I suppose you can reasonably assume any failures are due to Access Denied (however, I didn't look at all the possible return values from the win32 apis). However, I think you should at least add a comment about why you're not checking the lastError code. Applies to the other ones below as well.
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.
I'm not checking lastError because the documentation says If the function fails, the return value is NULL. Source and also because the other service classes called the function the same way.
I thought it would be better to have the potential exception speak for itself rather than assuming that the error is a Access Denied.
But I might be wrong. If I am, please tell me.
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.
Thanks for the explanation, I'm ok with this.
| ServiceResources.CouldNotGetServiceInfo, | ||
| ErrorCategory.PermissionDenied); | ||
| } | ||
| NativeMethods.SERVICE_DELAYED_AUTO_START_INFO autostartInfo = (NativeMethods.SERVICE_DELAYED_AUTO_START_INFO)Marshal.PtrToStructure(lpBuffer, typeof(NativeMethods.SERVICE_DELAYED_AUTO_START_INFO)); |
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.
I don't have a strong opinion on this one, but my preference is to have it as part of StartupType similar to what you see in services.msc
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.
Can we extend Enums using PowerShell somehow?
C# does not seem to be able to do it using just inheritance.
Currently the ServiceStartMode class does not support Automatic (Delayed start).
I could also make my own class, but that would be a breaking change, correct?
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.
Seems like having it separate would be simplest.
| // Description information | ||
| IntPtr lpBuffer = IntPtr.Zero; | ||
| DWORD bufferSizeNeeded = 0; | ||
| bool status = NativeMethods.QueryServiceConfig2W( |
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.
If seems like the QueryServiceConfig2W api along with the error handling could be an internal helper api since it's repeated pretty often.
| // General information | ||
| lpBuffer = IntPtr.Zero; | ||
| bufferSizeNeeded = 0; | ||
| status = NativeMethods.QueryServiceConfigW( |
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.
Same with QueryServiceConfigW as helper api
| if (IntPtr.Zero == hService) { | ||
| lastError = Marshal.GetLastWin32Error(); | ||
| Win32Exception exception = new Win32Exception(lastError); | ||
| WriteNonTerminatingError( |
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.
I think rather than writing a non-terminating error for each property that fails to be retrieved, it might be better to just write one non-terminating error if any fail.
|
|
||
| It "Get-Service can get the username of a service" { | ||
| try { | ||
| $userName = "user1" |
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.
Because there's very similar code for these test cases, you could turn them into -TestCases like:
# move user creation to BeforeAll{} and cleanup in AfterAll{}
It "Get-Service can get the <property> of a service" -TestCases @(
@{property="username"; value="user1"; parameters = @{ Credential = $creds },
@{property="binpath";value="$PSHOME\powershell.exe"},
...
){
param ($property, $value, $parameters)
if ($parameters -eq $null) {
$parameters = @{}
}
$parameters += @{Name=$servicename;binarypathname="...}
$service = New-Service @parameters
...
$service.$property | Should BeExactly $value
}|
I have converted the |
| // We set the initial value to an invalid value so that we can | ||
| // distinguish when this is and is not set. | ||
| internal ServiceStartMode startupType = (ServiceStartMode)(-1); | ||
| public ServiceStartupType StartupType { get; set; } = (ServiceStartupType)(-1); |
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.
It seems the change is not related to the PR purpose. I'd prefer to move it in separate PR.
| DisplayName, | ||
| Name, | ||
| exception, | ||
| "CouldNotNewServiceDescription", |
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.
Shouldn't this be related to DelayedAutoStart?
| DisplayName, | ||
| Name, | ||
| exception, | ||
| "CouldNotNewServiceDescription", |
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.
Shouldn't this be related to DelayedAutoStart?
|
Does it make sense to use ETS here? /cc @lzybkr |
|
It has been 3 days since the latest update on this issue, so I wanted to bring the issue up again. @iSazonov, you mention ETS. |
|
I think we can defer the ETS discussion and redo it if necessary. |
SteveL-MSFT
left a comment
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.
LGTM Thanks!
| /// Adds UserName, Description, BinPath and Delay Autostart info to the ServiceController object. | ||
| /// </summary> | ||
| /// <param name="service"></param> | ||
| /// <returns></returns> |
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.
Please remove or correct.
| ds.fDelayedAutostart = StartupType == ServiceStartupType.AutomaticDelayedStart; | ||
| size = Marshal.SizeOf(ds); | ||
| buffer = Marshal.AllocCoTaskMem(size); | ||
| Marshal.StructureToPtr(ds, buffer, false); |
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.
I wonder where we destroy and free?
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.
@daxian-dbw Could you please comment?
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.
Good catch! I don't find the allocated memory is freed anywhere. FreeCoTaskMem should be called once done using the memory. The same issue also happens in some other places in this file.
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.
I found AllocCoTaskMem in 4 files and FreeCoTaskMem in 17 files - It seems we should widely review. Maybe don't block the PR and open a tracking Issue for new PR?
| internal const string GetACPDllName = "api-ms-win-core-localization-l1-2-0.dll"; /*123*/ | ||
| internal const string DeleteServiceDllName = "api-ms-win-service-management-l1-1-0.dll"; /*124*/ | ||
| internal const string QueryServiceConfigDllName = "api-ms-win-service-management-l2-1-0.dll"; /*125*/ | ||
| internal const string QueryServiceConfig2DllName = "api-ms-win-service-management-l2-1-0.dll"; /*126*/ |
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.
Have we the ApiSet on Windows 7?
/cc @mirichmo
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.
The API set is included for Win 7
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 there MSDN public docs? I only see for Windows 10 and Windows 8.1. 😕
|
This is my first time freeing memory. I think I still have some memory which does not get dereferenced, but I just can't find it. I'm not sure if I should use AllocCoTaskMem or AllocHGlobal and I have searched a lot to figure out which one I should use. I would also free the memory when the other functions are called, but that is a different issue and I won't include it in this pull request. |
|
@joandrsn AllocHGlobal is from 16-bit Windows. See https://msdn.microsoft.com/en-us/library/aa366596.aspx I agree that we should open a tracking Issue to review our code for the problem with memory free. |
|
|
||
| #region ServiceStartupType | ||
| ///<summary> | ||
| ///Enum for usage with StartupType. Automatic, Manual & Disabled index matched from System.ServiceProcess.ServiceStartMode |
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.
Why is GitHub paints '&' in red?
| if ( -not $IsWindows ) { | ||
| $PSDefaultParameterValues["it:skip"] = $true | ||
| } | ||
| if($IsWindows) { |
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.
Please add spaces - use common formatting convention. Below too.
| { Remove-Service -Name "testremoveservice" -ErrorAction 'Stop' } | ShouldBeErrorId "InvalidOperationException,Microsoft.PowerShell.Commands.RemoveServiceCommand" | ||
| } | ||
|
|
||
| It "Get-Service can get the '<property>' of a service" -TestCases @( |
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.
Extra space before "-TestCases".
| $PSDefaultParameterValues["it:skip"] = $true | ||
| } | ||
| if($IsWindows) { | ||
| $userName = "testuserservices" |
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.
Please use indentation with 4 spaces. Below too.
SteveL-MSFT
left a comment
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.
Great! LGTM
| NativeMethods.SERVICE_CONFIG_DELAYED_AUTO_START_INFO, | ||
| buffer); | ||
|
|
||
| Marshal.FreeCoTaskMem(buffer); |
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.
I'd like to see this protected within a finally.
| ServiceResources.CouldNotNewServiceDelayedAutoStart, | ||
| ErrorCategory.PermissionDenied); | ||
| } | ||
| Marshal.FreeCoTaskMem(buffer); |
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.
Finally here as well
| DWORD dwDesiredAccess | ||
| ); | ||
|
|
||
| [DllImport(PinvokeDllNames.QueryServiceConfigDllName, CharSet = CharSet.Unicode, SetLastError = true)] |
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.
@daxian-dbw Are we still following the PInvokeDLLNames convention? It no longer makes sense since we stripped out the full CLR code
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.
@daxian-dbw Could you please comment?
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.
I think there is still one benefit -- you can easily know what native dependencies powershell has.
| cbBufSize: 0, | ||
| pcbBytesNeeded: out bufferSizeNeeded); | ||
|
|
||
| lpBuffer = Marshal.AllocCoTaskMem((int)bufferSizeNeeded); |
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.
A check should be made to confirm that status == ERROR_INSUFFICIENT_BUFFER before proceeding.
https://msdn.microsoft.com/en-us/library/windows/desktop/ms684935.aspx
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.
Interesting, what is best practice? Could we exclude second call by pre-allocate memory buffer?
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.
This technique is a common pattern in Windows APIs. It is designed to be called twice with the first call returning the buffer size needed so the correct buffer size can be allocated and supplied on the second call. You can do it all in one call, but you have to either guess how big the buffer should be or allocate a max size buffer. If you guess wrong, you'll need to resize it and call the method again anyway. A max size buffer will work, but will incur unnecessary memory allocation. With that in mind, it is optimal to call it twice and follow the expected pattern.
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.
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.
Agreed. Steve and I made the same review comment around the same time. This has been addressed.
| cbBufSize: 0, | ||
| pcbBytesNeeded: out bufferSizeNeeded); | ||
|
|
||
| lpBuffer = Marshal.AllocCoTaskMem((int)bufferSizeNeeded); |
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.
Same issue here. It should not proceed unless it gets the correct status value.
| } | ||
|
|
||
| bool queryStatus = NativeMethods.QueryServiceConfig2(hService, NativeMethods.SERVICE_CONFIG_DESCRIPTION, out descriptionStructPtr); | ||
| NativeMethods.SERVICE_DESCRIPTIONW description = (NativeMethods.SERVICE_DESCRIPTIONW)Marshal.PtrToStructure(descriptionStructPtr, typeof(NativeMethods.SERVICE_DESCRIPTIONW)); |
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.
I'd like to see these method calls organized differently to protect against potential memory leaks. The pattern is repeated in NativeMethods.QueryServiceConfig2 and I'd like all instances of it fixed. Since NativeMethods.QueryServiceConfig() is a helper function, its output parameter, instead of being an IntPtr structurePointer, should be the managed object NativeMethods.SERVICE_DESCRIPTIONW. That way, the unmanaged memory that is allocated within that function is also cleaned up within that function. The managed object carries the values out of the function and eventually gets garbage collected. To fix this instance, line 741 should be moved within the helper function. You can probably make Nativemethods.QueryServiceConfig2() a generic method as well to avoid having to use a switch for infoLevel.
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.
This sounds like a great idea and I'm all for it.
Could you describe a bit more what you would change?
Like somewhere with an example.
The problem I'm having now is if I return the struct as an out, I have to set the variable before returning. I could set default values, but that means I have to check the values when adding the properties to my Service-object.
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.
@mirichmo, could you comment on my last comment?
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.
The idea is to put (NativeMethods.QUERY_SERVICE_CONFIG)Marshal.PtrToStructure(serviceConfigStructPtr, typeof(NativeMethods.QUERY_SERVICE_CONFIG)); and Marshal.FreeCoTaskMem in QueryServiceConfig2 and return the managed object NativeMethods.SERVICE_DESCRIPTIONW. On error return Null.
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.
I had something like this in mind. The code is a bit messy and should be cleaned up, but illustrates the idea. The idea is that the helper function is a generic method that handles all the types in a similar manner. It also keeps allocation and freeing within the same method.
Method
internal static bool QueryServiceConfig2<T>(NakedWin32Handle hService, DWORD infolevel, out T configStructure)
{
IntPtr lpBuffer = IntPtr.Zero;
structurePointer = IntPtr.Zero;
DWORD bufferSize, bufferSizeNeeded = 0;
bool status = NativeMethods.QueryServiceConfig2W(
hService: hService,
dwInfoLevel: infolevel,
lpBuffer: lpBuffer,
cbBufSize: 0,
pcbBytesNeeded: out bufferSizeNeeded);
if (status != true && Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER) {
return status;
}
try {
lpBuffer = Marshal.AllocCoTaskMem((int)bufferSizeNeeded);
bufferSize = bufferSizeNeeded;
status = NativeMethods.QueryServiceConfig2W(
hService,
infolevel,
lpBuffer,
bufferSize,
out bufferSizeNeeded);
configStructure = (T)Marshal.PtrToStructure(lpBuffer, typeof(T));
}
finally
{
Marshal.FreeCoTaskMem(lpBuffer);
}
return status;
}Caller
NativeMethods.SERVICE_DESCRIPTIONW descriptionStruct = new NativeMethods.SERVICE_DESCRIPTIONW();
bool querySuccessful = NativeMethods.QueryServiceConfig2<NativeMethods.SERVICE_DESCRIPTIONW>(hService, NativeMethods.SERVICE_CONFIG_DESCRIPTION, out descriptionStruct);
// No free is needed hereThere 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.
Thank you! 👍 I made the updates. Could you please review @mirichmo
… moved to the finally block in the last commit
iSazonov
left a comment
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.
Leave a comment
| internal const string GetACPDllName = "api-ms-win-core-localization-l1-2-0.dll"; /*123*/ | ||
| internal const string DeleteServiceDllName = "api-ms-win-service-management-l1-1-0.dll"; /*124*/ | ||
| internal const string QueryServiceConfigDllName = "api-ms-win-service-management-l2-1-0.dll"; /*125*/ | ||
| internal const string QueryServiceConfig2DllName = "api-ms-win-service-management-l2-1-0.dll"; /*126*/ |
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 there MSDN public docs? I only see for Windows 10 and Windows 8.1. 😕
| { Remove-Service -Name "testremoveservice" -ErrorAction 'Stop' } | ShouldBeErrorId "InvalidOperationException,Microsoft.PowerShell.Commands.RemoveServiceCommand" | ||
| } | ||
|
|
||
| It "Get-Service can get the '<property>' of a service" -TestCases @( |
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.
Closed.
| } | ||
|
|
||
| bool querySuccessful = NativeMethods.QueryServiceConfig2(hService, NativeMethods.SERVICE_CONFIG_DESCRIPTION, out descriptionStructPtr); | ||
| NativeMethods.SERVICE_DESCRIPTIONW description = (NativeMethods.SERVICE_DESCRIPTIONW)Marshal.PtrToStructure(descriptionStructPtr, typeof(NativeMethods.SERVICE_DESCRIPTIONW)); |
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.
@joandrsn Could you please address @mirichmo previous request:
I'd like to see these method calls organized differently to protect against potential memory leaks. The pattern is repeated in NativeMethods.QueryServiceConfig2 and I'd like all instances of it fixed. Since NativeMethods.QueryServiceConfig() is a helper function, its output parameter, instead of being an IntPtr structurePointer, should be the managed object NativeMethods.SERVICE_DESCRIPTIONW. That way, the unmanaged memory that is allocated within that function is also cleaned up within that function. The managed object carries the values out of the function and eventually gets garbage collected. To fix this instance, line 741 should be moved within the helper function. You can probably make Nativemethods.QueryServiceConfig2() a generic method as well to avoid having to use a switch for infoLevel.
|
@mirichmo Could you please take another look? |
mirichmo
left a comment
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.
Just one small comment. Other than that this looks good and it almost ready for merge.
| // We set the initial value to an invalid value so that we can | ||
| // distinguish when this is and is not set. | ||
| internal ServiceStartMode startupType = (ServiceStartMode)(-1); | ||
| internal ServiceStartupType startupType = (ServiceStartupType)(-1); |
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.
Sorry, I just noticed this. Your use of a wrapper enum + conversion functions makes it possible to define -1 as an ServiceStartupType value so it does not need to be casted throughout. Something like ServiceStartupType.InvalidValue or ServiceStartupType.Undefined would work.
…o get-service-username
|
Thanks @mirichmo! I believe I fixed it. |
|
@joandrsn Please add |
|
@iSazonov did I understand you correctly? I just updated my initial PR comment to reflect how |
|
LGTM. |
|
@SteveL-MSFT We've got new commits - could you please take another look? |
|
@joandrsn Many thanks for your contribution! Welcome back with new PRs! |
|
Whatever happened to this? It looks like it never made it into a final build as I'm not seeing it in 7.0.3. |
|
@jcoffi You can see all properties with: Get-Service wuauserv | fl * -Force |
I had the time to look into this issue #2579
I have added the following to the
Get-Servicecommand:ServicePath)LogOnAs)The ServiceStartupType was also added, providing the user access to use services like in
services.msc, havingAutomatic,Automatic (Delayed Start),ManualandDisabledas options (Also includesInvalidValueas an initial value forServiceStartupType)I'm not completely sure if my naming of the variables are correct and would like some feedback.
I have tried to get a complete overview of what the different properties are called in the different ways to interact with services (With my additions marked like this).
*Only username of the account running the service.
When adding the properties, I used NotePropertie and I don't know if it's possible to make autocomplete for this.
I would love to have the ability to type:
$service = Get-Service -Name 'spooler'$service.Use<TAB>=>$service.UserNameAnother thing: I'm not sure if I should/can split my function for adding properties up into smaller pieces since it's about 100 lines currently.
I might remove DelayedAutoStart from the pull request since people might rather want it as a StartupType instead. I currently have it in since people might find the information useful on its own.