Skip to content

Conversation

@joandrsn
Copy link
Contributor

@joandrsn joandrsn commented Sep 23, 2017

I had the time to look into this issue #2579

I have added the following to the Get-Service command:

  • BinaryPathName (Was suggested as ServicePath)
  • Description
  • UserName (Was suggested as LogOnAs)
  • DelayedAutoStart
  • ServiceStartupType

The ServiceStartupType was also added, providing the user access to use services like in services.msc, having Automatic, Automatic (Delayed Start), Manual and Disabled as options (Also includes InvalidValue as an initial value for ServiceStartupType)

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).

New-Service Set-Service Get-Service Get-CimInstance
N/A Computername N/A SystemName?
Name Name Name Name
DisplayName DisplayName DisplayName DisplayName/Caption
Description Description Description Description
StartupType StartupType StartupType Startmode (string)
N/A Status Status State
Credentials Credentials UserName* StartName*
BinaryPathName N/A BinaryPathName PathName
DependsOn N/A RequiredServices N/A
N/A N/A N/A DelayedAutoStart
N/A N/A N/A ProcessId

*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.UserName

Another 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.

dwDesiredAccess: NativeMethods.SC_MANAGER_CONNECT
);
if (IntPtr.Zero == hScManager) {
lastError = Marshal.GetLastWin32Error();
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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));
Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Member

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(
Copy link
Member

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(
Copy link
Member

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(
Copy link
Member

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"
Copy link
Member

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
}

@joandrsn
Copy link
Contributor Author

I have converted the StartupType to a custom enum. It's now possible to use to specify AutomaticDelayedStart as a StartupType.

// 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);
Copy link
Collaborator

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",
Copy link
Member

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",
Copy link
Member

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?

@iSazonov
Copy link
Collaborator

Does it make sense to use ETS here?

/cc @lzybkr

@joandrsn
Copy link
Contributor Author

It has been 3 days since the latest update on this issue, so I wanted to bring the issue up again.
Are we waiting for @lzybkr to review this?

@iSazonov, you mention ETS.
I initially tried to use ETS but it seemed at bit hard. Then I saw what pull request #2850 did it and used the Properties-method.
Are there any benifits to using ETS over using Properties directly?
Or does it generate more readable code?
Or does it maybe help PowerShell recognize properties and then enable tab-completion of the properties added?
I am curious :)

@SteveL-MSFT
Copy link
Member

I think we can defer the ETS discussion and redo it if necessary.

Copy link
Member

@SteveL-MSFT SteveL-MSFT left a 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>
Copy link
Collaborator

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);
Copy link
Collaborator

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?

Copy link
Collaborator

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?

Copy link
Member

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.

Copy link
Collaborator

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*/
Copy link
Collaborator

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

Copy link
Member

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

Copy link
Collaborator

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. 😕

@joandrsn
Copy link
Contributor Author

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.
If I run this command:
1..10000 | % { $null = Get-Service -Name 'spooler' }
I am not getting any less memory allocated, but only more (Very minor, but still relevant).

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.
Currently, I'm using both, but I should probably use only one of the methods since it gives better readability.
I am however using the correct method for freeing memory, as I should not mix.

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.

@iSazonov
Copy link
Collaborator

@joandrsn AllocHGlobal is from 16-bit Windows. See https://msdn.microsoft.com/en-us/library/aa366596.aspx
AllocCoTaskMem is recommended.

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
Copy link
Collaborator

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) {
Copy link
Collaborator

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 @(
Copy link
Collaborator

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"
Copy link
Collaborator

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.

Copy link
Member

@SteveL-MSFT SteveL-MSFT left a 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);
Copy link
Member

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);
Copy link
Member

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)]
Copy link
Member

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

Copy link
Collaborator

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?

Copy link
Member

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);
Copy link
Member

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

Copy link
Collaborator

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?

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find this. I think your comment might be on an outdated version of the code. I made the check here and here

Copy link
Member

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);
Copy link
Member

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));
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Collaborator

@iSazonov iSazonov Oct 10, 2017

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.

Copy link
Member

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 here

Copy link
Contributor Author

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

Copy link
Collaborator

@iSazonov iSazonov left a 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*/
Copy link
Collaborator

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 @(
Copy link
Collaborator

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));
Copy link
Collaborator

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.

@lzybkr lzybkr removed their request for review October 10, 2017 01:55
@iSazonov
Copy link
Collaborator

@mirichmo Could you please take another look?

Copy link
Member

@mirichmo mirichmo left a 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);
Copy link
Member

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.

@joandrsn
Copy link
Contributor Author

Thanks @mirichmo! I believe I fixed it.

@iSazonov iSazonov added the Documentation Needed in this repo Documentation is needed in this repo label Oct 21, 2017
@iSazonov
Copy link
Collaborator

@joandrsn Please add ServiceStartupType.InvalidValue in the PR description.

@joandrsn
Copy link
Contributor Author

@iSazonov did I understand you correctly? I just updated my initial PR comment to reflect how ServiceStartupType can be used.

@iSazonov
Copy link
Collaborator

LGTM.

@iSazonov
Copy link
Collaborator

@SteveL-MSFT We've got new commits - could you please take another look?

@iSazonov iSazonov merged commit ca30054 into PowerShell:master Oct 24, 2017
@iSazonov
Copy link
Collaborator

@joandrsn Many thanks for your contribution! Welcome back with new PRs!

@jcoffi
Copy link

jcoffi commented Sep 22, 2020

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.

@iSazonov
Copy link
Collaborator

@jcoffi You can see all properties with:

Get-Service wuauserv | fl * -Force

@joandrsn joandrsn deleted the get-service-username branch April 23, 2021 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants