Added BinPath, Description, UserName and Delayed Auto Start to Get-Service#4907
Added BinPath, Description, UserName and Delayed Auto Start to Get-Service#4907iSazonov merged 21 commits intoPowerShell:masterfrom
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Shouldn't this be related to DelayedAutoStart?
| DisplayName, | ||
| Name, | ||
| exception, | ||
| "CouldNotNewServiceDescription", |
There was a problem hiding this comment.
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. |
| /// 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.
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.
I wonder where we destroy and free?
There was a problem hiding this comment.
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.
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.
Have we the ApiSet on Windows 7?
/cc @mirichmo
There was a problem hiding this comment.
The API set is included for Win 7
There was a problem hiding this comment.
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.
Why is GitHub paints '&' in red?
| if ( -not $IsWindows ) { | ||
| $PSDefaultParameterValues["it:skip"] = $true | ||
| } | ||
| if($IsWindows) { |
There was a problem hiding this comment.
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.
Extra space before "-TestCases".
| $PSDefaultParameterValues["it:skip"] = $true | ||
| } | ||
| if($IsWindows) { | ||
| $userName = "testuserservices" |
There was a problem hiding this comment.
Please use indentation with 4 spaces. Below too.
| NativeMethods.SERVICE_CONFIG_DELAYED_AUTO_START_INFO, | ||
| buffer); | ||
|
|
||
| Marshal.FreeCoTaskMem(buffer); |
There was a problem hiding this comment.
I'd like to see this protected within a finally.
| ServiceResources.CouldNotNewServiceDelayedAutoStart, | ||
| ErrorCategory.PermissionDenied); | ||
| } | ||
| Marshal.FreeCoTaskMem(buffer); |
| DWORD dwDesiredAccess | ||
| ); | ||
|
|
||
| [DllImport(PinvokeDllNames.QueryServiceConfigDllName, CharSet = CharSet.Unicode, SetLastError = true)] |
There was a problem hiding this comment.
@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.
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.
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.
Interesting, what is best practice? Could we exclude second call by pre-allocate memory buffer?
There was a problem hiding this comment.
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.
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.
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.
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.
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.
@mirichmo, could you comment on my last comment?
There was a problem hiding this comment.
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.
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.
Thank you! 👍 I made the updates. Could you please review @mirichmo
… moved to the finally block in the last commit
| 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.
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 @( |
| } | ||
|
|
||
| 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.
@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.
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.
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.