Bienvenido a nuestra nueva serie de PowerShell, dedicada a trucos de PowerShell y a cómo incorporarlos en PowerShell Universal. Así es como funciona esta serie:
- La primera parte de cada artículo trata técnicas de PowerShell puro y le presenta trucos sorprendentes y ejemplos de código que elevan sus conocimientos de PowerShell.
- La segunda parte utiliza el nivel gratuito de PowerShell Universal para construir su propio panel personal de PowerShell, de modo que pueda utilizar fácilmente su código de PowerShell sin necesidad de buscar scripts ni ejecutar consolas.
PowerShell Universal (PSU)
Si no conoce PowerShell Universal, este es el lugar perfecto para empezar. PowerShell Universal integra código de PowerShell como un servidor web y, con solo unos clics, puede empezar a crear sus propios paneles con interfaz gráfica. Complementa a la perfección sus conocimientos existentes de PowerShell y funciona en múltiples plataformas, tanto con Windows PowerShell como con PowerShell 7. Con él, puede crear paneles y herramientas sin recurrir a técnicas complejas como WPF, WinForms u otras interfaces gráficas.
Escalado de funciones de PowerShell
Veamos primero algo de código de PowerShell y apliquemos algunos trucos y técnicas para mejorarlo.
¿Alguna vez ha querido identificar qué proceso aloja un servicio del sistema concreto, solo para darse cuenta de que todos aparecen como «svchost.exe» en Process Monitor o Process Explorer?
La solución implica dos funciones de PowerShell que recuperan los identificadores de proceso de los servicios y los nombres de los grupos de servicios, entre otras muchas cosas. Funcionan de maravilla, pero son lentas a gran escala. Véalo usted mismo:
function Get-ServiceProcessId
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string] $Name
)
process
{
$svc = Get-CimInstance Win32_Service -Filter "Name='$Name'"
if ($svc -and ($svc.ProcessId -ne 0))
{
$svc.ProcessId
}
}
}
Esta magnífica función toma un nombre de proceso y devuelve su identificador de proceso (si está disponible):
PS C:\> Get-ServiceProcessId -Name spooler
4784
Como esta función admite canalización (pipeline), puede escalarla a muchos procesos:
PS C:\> Get-Service | Get-ServiceProcessId
5524
1960
(...)
En esta llamada, Get-Service vuelca todos los servicios y Get-ServiceProcessId vincula automáticamente la propiedad «Name» a su parámetro «Name» (ValueFromPipelineByPropertyName). Como el código de la función está incluido en un bloque process {}, este código se repite para cada elemento de la canalización. Escribir funciones de este tipo, compatibles con la canalización, no tiene ningún misterio: designe un parámetro con ValueFromPipeline o ValueFromPipelineByPropertyName, e incluya su código en process {}; ya está. Ahora su función escala de forma automática.
Sin embargo, la llamada es exasperantemente lenta: los identificadores de proceso van llegando uno a uno con segundos de retraso. Esto se debe a que el código de la función consulta WMI para cada servicio, y esta consulta WMI lleva tiempo.
Optimización del rendimiento
El problema de rendimiento que acaba de ver es habitual en muchos scripts. Siempre que se consulta información, ya sea de WMI, Active Directory, bases de datos o cualquier otra fuente, el código no escala bien.
Una mejora sencilla para estos escenarios consiste en crear tablas de referencia (lookup tables). PowerShell ya proporciona un cmdlet que hace esto por usted. Véalo:
function Get-ServiceProcessId
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string] $Name
)
begin
{
$hash = Get-CimInstance Win32_Service -ErrorAction Ignore |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
$id = $hash.$Name.ProcessId
if ($id) { $id }
}
}
Esta función es extremadamente rápida: cuando consulta varios servicios, la consulta WMI se ejecuta una sola vez. Tras el retraso inicial provocado por la consulta, los identificadores de proceso solicitados se vuelcan sin retrasos adicionales.
Esto se debe a que hemos añadido un bloque begin {} a la función. Esta parte se ejecuta una única vez, como preparación, antes de que suceda cualquier otra cosa. En este bloque, consultamos WMI para todos los servicios, algo que en realidad no lleva mucho más tiempo que preguntar por un único servicio.
Para poder encontrar más adelante la información de un servicio concreto, los resultados de la consulta se pasan a Group-Object. Este cmdlet es perfecto para crear tablas de referencia porque admite los parámetros -AsHashtable y -AsString, que crean automáticamente una tabla hash indexada por la propiedad que haya especificado. ¿Suena complejo? No lo es. Véalo:
PS C:\> $hashtable = Get-CimInstance Win32_Service | Group-Object -Property Name -AsHashTable -AsString
PS C:\> $hashtable.Spooler.processId
4784
PS C:\> $hashtable.WSearch.ProcessId
10152
PS C:\> $hashtable.WSearch
ProcessId Name StartMode State Status ExitCode
--------- ---- --------- ----- ------ --------
10152 WSearch Auto Running OK 0
Con la tabla hash generada por Group-Object, los nombres de las claves son ahora los nombres de los servicios. Cuando especifica el nombre de un servicio concreto, obtiene el objeto de servicio. Cuando añade una propiedad específica, por ejemplo .ProcessId, obtiene directamente su identificador de proceso.
Veamos otro ejemplo para que quede claro antes de continuar:
PS C:\> $hashtable = Get-Service | Group-Object -Property Status -AsHashTable -AsString
PS C:\> $hashtable.Running.Count
156
PS C:\> $hashtable.Stopped.Count
170
PS C:\> $hashtable.Running
Status Name DisplayName
------ ---- -----------
Running AdobeARMservice Adobe Acrobat Update Service
Running AppIDSvc Application Identity
Running Appinfo Application Information
(...)
Esta vez hemos agrupado por la propiedad «Status», y ahora podemos usar cualquiera de las constantes de Status para consultar la tabla hash. Como hay más de un servicio con el estado «Running» o «Stopped», en este caso cada clave de la tabla hash devuelve varios servicios. Ahora resulta trivial contar el número de servicios en ejecución o detenidos, o volcar todos los servicios en ejecución. El concepto de agrupación con tablas hash es muy flexible y extremadamente potente.
Sin embargo, si se para a pensarlo un momento, esto plantea otro reto: ahora que puede escalar su función y recuperar rápidamente los identificadores de proceso de muchos servicios, ¿de qué sirve una lista de números si se pierde el nombre del servicio original al que pertenece un identificador de proceso concreto?
Añadir información
Para añadir información —en lugar de sustituirla—, PowerShell dispone de otro cmdlet pensado exactamente para este propósito. Esta versión mejorada puede añadir el identificador de proceso con su nuevo parámetro -PassThru:
function Get-ServiceProcessId {
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]
[string]
$Name,
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]
[System.ServiceProcess.ServiceController]
$Service,
[switch]
$PassThru
)
begin
{
$hash = Get-CimInstance Win32_Service -ErrorAction Ignore |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
# since the user now has the option to either specify the service name
# or pipe in an actual service, let's look up the pendant so we always
# have both and can simplify the remaining code:
if ($PSCmdlet.ParameterSetName -eq 'ServiceName')
{
$Service = Get-Service -Name $Name -ErrorAction Ignore
}
else
{
$Name = $Service.Name
}
# make sure "0" values are replaced by NULL
$id = $hash.$Name.ProcessId | Where-Object { $_ -gt 0 }
if ($PassThru)
{
# take the service and add the process ID
$Service |
Add-Member -MemberType NoteProperty -Name ProcessId -Value $id -PassThru
}
else
{
$id
}
}
}
Puede encontrar el parámetro -PassThru en bastantes cmdlets de PowerShell, y cuando está presente, sabe que tiene la opción de mostrar como salida un valor recién calculado, o añadir el nuevo valor a la entrada original. Esto se entiende mejor observando los resultados. Por defecto, no cambia nada y sigue obteniendo los identificadores de proceso de los servicios:
PS C:\> Get-Service | Get-ServiceProcessId
5524
1960
(...)
Si especifica -PassThru, obtiene los objetos de entrada originales (los servicios que ha canalizado):
PS C:\> Get-Service | Get-ServiceProcessId -PassThru
Status Name DisplayName
------ ---- -----------
Stopped AarSvc_78962bb Agent Activation Runtime_78962bb
Running AdobeARMservice Adobe Acrobat Update Service
Stopped ADPSvc Aggregated Data Platform Service
(...)
Sin embargo, estos objetos ahora tienen una nueva propiedad llamada ProcessId que puede usar para ordenar. Para hacerla visible, use Select-Object:
PS C:\> Get-Service | Get-ServiceProcessId -PassThru | Sort-Object ProcessId -Descending | Select-Object -Property DisplayName, ProcessId
DisplayName ProcessId
----------- ---------
Web Threat Defense Service 19084
Diagnostic System Host 18580
Benutzerdienst für die Plattform für verbundene Ge... 17608
WebClient 16616
Quality Windows Audio Video Experience 15328
(...)
Y así es como funciona:
Para que una función pueda transmitir información, necesita acceso al objeto de entrada original. Por eso la nueva función ahora tiene dos parámetros mutuamente excluyentes: -Name (especifique un nombre de servicio como «Spooler») y -Service (proporcione una instancia real de un servicio). Internamente, la función detecta qué información ha proporcionado el usuario y calcula la que falta.
Cuando el usuario especifica -PassThru, el objeto de servicio se canaliza a Add-Member. Add-Member puede añadir dinámicamente nuevas propiedades a cualquier objeto. En este ejemplo, añade una nueva «NoteProperty» (valor estático) llamada ProcessId y almacena en ella el identificador de proceso recién recuperado. Eso es todo.
Una vez que domine esta técnica, podrá diseñar su código de forma mucho más modular, como descubriremos a continuación.
Sistema flexible tipo Lego
Como práctica, añadamos dos funciones más que averigüen el nombre del proceso a partir de un identificador de proceso concreto, y el nombre del grupo de servicios. Es el mismo planteamiento básico, usando los mismos ingredientes que acabamos de comentar.
Añadir el nombre del proceso
Aquí tiene una función modular de PowerShell que recupera el nombre del proceso en texto claro a partir de cualquier objeto que tenga una propiedad llamada ProcessId o Id de tipo entero:
function Get-ProcessNameById {
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[Object]
$InputObject,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[int]
[Alias('Id')]
$ProcessId,
[switch]
$PassThru
)
process
{
$ProcessName = if ($ProcessId)
{
(Get-Process -Id $ProcessId).Name
}
if ($PassThru)
{
$InputObject |
Add-Member -MemberType NoteProperty -Name ProcessName -Value $ProcessName -PassThru
}
else
{
$ProcessName
}
}
}
Repasemos rápidamente los trucos de codificación de PowerShell utilizados aquí:
- El bloque
param()hace referencia al objeto de entrada dos veces: aparece sin cambios en$InputObject(para poder transmitirlo más adelante si se solicita), y su identificador de proceso aparece en$ProcessId. PowerShell hace la parte difícil e identifica la propiedad adecuada. Usted solo especifica el tipo deseado ([int]en este ejemplo) y los nombres válidos (ProcessId,Id).[Alias()]acepta una lista separada por comas, de modo que puede especificar tantos nombres de propiedad como desee. Cualquiera de ellos es válido para este parámetro, y el primero que coincida es el que se usa. - Si se encontró un identificador de proceso en el objeto de entrada, este aparece en
$ProcessId. El código utiliza un simpleIfpara traducirlo al nombre del proceso. Sin embargo, si$ProcessIdes NULL (porque faltaba la propiedad en el objeto de entrada o estaba vacía),$ProcessNamees NULL. El truco aquí consiste en asignar$ProcessNameal resultado delIf, y no asignarlo dentro de la cláusulaIf, de modo que quede en null cuando no se cumpla la condición delIf. Observe qué elegante y genérica resulta la nueva función. Puede usarla con cualquier objeto de entrada que tenga una propiedadProcessIdoId. Por ejemplo, averigüe el nombre de su host actual de PowerShell:
PS C:\> Get-Process -id $pid | Get-ProcessNameById
powershell_ise
O aplíquela a nuestros servicios, y obtenga una lista de servicios, sus procesos subyacentes e identificadores de proceso:
PS C:\> Get-Service | Get-ServiceProcessId -PassThru | Get-ProcessNameById -PassThru | Where-Object ProcessId | Select-Object -Property Name, ProcessName, ProcessId | select -First 5
Name ProcessName ProcessId
---- ----------- ---------
AdobeARMservice armsvc 5524
AppIDSvc svchost 1960
Appinfo svchost 13852
AudioEndpointBuilder svchost 4324
Audiosrv svchost 4456
(...)
Grupos de servicios
Como habrá notado, muchos servicios comparten el mismo proceso, y hay muchos procesos con nombres idénticos («svchost»). Esto es intencionado: en Windows, muchos servicios pueden ejecutarse dentro de la misma DLL.
Para recuperar su grupo de servicios, podemos reutilizar Get-ServiceProcessId. Esta vez, no leemos el identificador de proceso de los objetos WMI, sino PathName:
function Get-ServiceGroupName
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]
[string]
$Name,
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]
[System.ServiceProcess.ServiceController]
$Service,
[switch]
$PassThru
)
begin
{
$hash = Get-CimInstance Win32_Service |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
# since the user now has the option to either specify the service name
# or pipe in an actual service, let's look up the pendant so we always
# have both and can simplify the remaining code
if ($PSCmdlet.ParameterSetName -eq 'ServiceName')
{
$Service = Get-Service -Name $Name -ErrorAction Ignore
}
else
{
$Name = $Service.Name
}
# get the launch command for this service
$pathName = $hash.$Name.PathName
# look if the parameter "-k" was specified, followed by the service group name
# we are after (uses RegEx)
$groupName = if($pathName -match '-k\s+(\w+)')
{
$matches[1]
}
if ($PassThru)
{
# take the service and add the process ID
$Service |
Add-Member -MemberType NoteProperty -Name GroupName -Value $groupName -PassThru
}
else
{
$groupName
}
}
}
Y como hemos usado un scripting modular compatible con canalización, obtenemos otra pieza de código tipo Lego que podemos combinar con las demás según sea necesario:
PS C:\> Get-Service | Get-ServiceProcessId -PassThru | Get-ProcessNameById -PassThru | Get-ServiceGroupName -PassThru | Where-Object ProcessId | Select-Object -Property Name, ProcessName, GroupName, ProcessId
Name ProcessName GroupName ProcessId
---- ----------- --------- ---------
AdobeARMservice armsvc 5524
AppIDSvc svchost LocalServiceNetworkRestricted 1960
Appinfo svchost netsvcs 13852
AudioEndpointBuilder svchost LocalSystemNetworkRestricted 4324
Audiosrv svchost LocalServiceNetworkRestricted 4456
(...)
PowerShell Universal al rescate
Puede que haya invertido varias horas de trabajo y haya creado excelentes scripts de PowerShell que le proporcionan la información que necesita. Pero ¿se acordará de todo esto la semana que viene? ¿El mes que viene? ¿Qué ocurre si necesita esta información en producción? ¿O si quiere que un compañero de soporte técnico la consulte sin tener que enseñarle consolas y scripts de PowerShell?
Ahí es donde entra en juego PowerShell Universal. Es completamente gratuito para uso personal, y si desea usarlo comercialmente, tiene una licencia económica por servidor, no por puesto.
En resumen, PowerShell Universal integra un host de PowerShell en un servidor web. Esto le permite crear de forma segura paneles ricos en interfaz gráfica, convertir scripts en servicios y servicios web, y mucho más.
Por ahora, queremos usar la edición personal gratuita para crear un panel con interfaz gráfica rica donde podamos consultar la información de servicios que acabamos de recuperar. Tenga en cuenta que esto solo era un ejemplo; evidentemente, puede sustituir nuestro código por cualquier cosa que le resulte más relevante y mostrar servidores VMWare, colecciones de imágenes o cuentas de usuario de la misma manera.
Configuración de PSU
Estos son los pocos pasos necesarios para configurar PSU en su propio equipo. Sin licencia, sin coste, sin necesidad de privilegios de administrador. Puede usar el módulo PowerShell Universal para instalar el servidor Universal. Para instalar el módulo, use Install-Module o Install-PSResource.
Install-Module Devolutions.PowerShellUniversal
Para instalar el servidor Universal, puede usar Install-PSUServer.
Install-PSUServer -LatestVersion
Ejecutar este comando tiene efectos diferentes según su sistema operativo:
- En Windows, crea e inicia un servicio de Windows en su equipo.
- En Linux, crea e inicia un servicio systemd.
- En macOS, descarga y extrae el servidor PowerShell Universal. Para conocer opciones de instalación adicionales, consulte la documentación en Installation | PowerShell Universal.
Creación de una aplicación PSU
Para añadir una interfaz gráfica a nuestro script, navegue a http://localhost:5000 para abrir la consola de administración de PSU. Debe iniciar sesión con la cuenta de Devolutions que creó anteriormente.
En el lado izquierdo, haga clic en «Apps» y luego en «App». En el lado derecho verá ahora una lista de las aplicaciones que ha creado anteriormente. Al principio, esta lista está vacía.
Haga clic en «Create App». Asígnele un nombre descriptivo, por ejemplo, «Service Dashboard». En el campo situado debajo, introduzca la «URL» de su aplicación. Debe ser un nombre relativo, precedido de una barra diagonal «/», por ejemplo, «/list-services». Puede dejar «Description» vacío. Asegúrese de que el deslizador «Definition» muestre «Code», no «Command». A continuación, haga clic en OK. La nueva aplicación aparece en su lista de aplicaciones.
A continuación, haga clic en el icono del lápiz. Esto abre el editor de PowerShell integrado. El código predeterminado es este:
New-UDApp -Content { 'Hello, World!' }
Para probar esta aplicación, haga clic en el botón «View App» situado en la parte superior. Su script de PowerShell basado en la web se ejecuta y verá un mensaje amistoso en su navegador. También verá cómo acceder a esta aplicación: su URL es http://localhost:5000/list-services/Home.
Advertencias
Antes de continuar, aquí tiene dos cosas importantes que recordar:
- El código de ejemplo predeterminado usa
New-UDApp {}para mostrar un mensaje sencillo. Este cmdlet siempre debe estar presente, ya que es lo que crea la aplicación. Todos sus ajustes se realizan únicamente dentro de las llaves. - Siempre puede probar su código haciendo clic en el botón «View App» en la parte superior de la página del editor, pero primero debe hacer clic en el icono GUARDAR. «View App» no guarda automáticamente sus últimas ediciones. Si olvida hacer clic en GUARDAR, «View App» ignorará sus últimos cambios en el código.
Mostrar una tabla
Sustituya el código predeterminado dentro de las llaves por este código:
New-UDApp -Content {
# 1.) prepare the data to show
# get all services...
$data = Get-Service -ErrorAction Ignore |
Sort-Object -Property DisplayName |
# ...select the properties to display in the table
Select-Object -Property DisplayName, Name, StartMode, StartType
# 2.) output it using one of the many available UI components, i.e. as a table
New-UDTable -Data $data
}
El código recupera cierta información y la almacena en $data. A continuación, llama a uno de los muchos cmdlets UD… para mostrar los datos en su aplicación web. New-UDTable, por ejemplo, crea automáticamente una tabla por usted.
Una vez que haga clic en «View App», podrá mostrar la tabla en su navegador:
Puede que haya notado que hemos añadido -ErrorAction Ignore a nuestro código. Esto se debe a que, por defecto, PSU ejecuta su código en PowerShell 7. PowerShell 7 se comporta de forma ligeramente distinta a Windows PowerShell, y Get-Service puede generar excepciones en sistemas que no son de EE. UU. Siempre que se generen excepciones en su código, la aplicación mostrará estos mensajes de error. Para evitarlo, asegúrese siempre de que su código no genere excepciones sin controlar.
Visor de servicios
Ahora que ha visto lo sencillo que es añadir una interfaz de usuario gráfica basada en el navegador a su código, cerremos el círculo y creemos nuestro visor extendido de procesos de servicios. Sustituya el código por el que creamos anteriormente:
# first, define the functions we created
# these functions could also be stored in external modules so they are
# out of the way, but for now we keep everything as a single script
# submit name of service or pipe in services
# adds the process ID of the process that executes a service
function Get-ServiceProcessId {
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]
[string]
$Name,
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]
[System.ServiceProcess.ServiceController]
$Service,
[switch]
$PassThru
)
begin
{
$hash = Get-CimInstance Win32_Service -ErrorAction Ignore |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
# since the user now has the option to either specify the service name
# or pipe in an actual service, let's look up the pendant so we always
# have both and can simplify the remaining code
if ($PSCmdlet.ParameterSetName -eq 'ServiceName')
{
$Service = Get-Service -Name $Name -ErrorAction Ignore
}
else
{
$Name = $Service.Name
}
# make sure "0" values are replaced by NULL
$id = $hash.$Name.ProcessId | Where-Object { $_ -gt 0 }
if ($PassThru)
{
# take the service and add the process ID
$Service |
Add-Member -MemberType NoteProperty -Name ProcessId -Value $id -PassThru
}
else
{
$id
}
}
}
# pipe in any object with a property "ProcessId" or "Id", and add a property with
# the actual process name
function Get-ProcessNameById {
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[Object]
$InputObject,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[int]
[Alias('Id')]
$ProcessId,
[switch]
$PassThru
)
process
{
$ProcessName = if ($ProcessId)
{
(Get-Process -Id $ProcessId).Name
}
if ($PassThru)
{
$InputObject |
Add-Member -MemberType NoteProperty -Name ProcessName -Value $ProcessName -PassThru
}
else
{
$ProcessName
}
}
}
# submit the name of a service, or pipe in services
# adds the property "GroupName", exposing the name of the service group for
# the given service
function Get-ServiceGroupName
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]
[string]
$Name,
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]
[System.ServiceProcess.ServiceController]
$Service,
[switch]
$PassThru
)
begin
{
$hash = Get-CimInstance Win32_Service |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
# since the user now has the option to either specify the service name
# or pipe in an actual service, let's look up the pendant so we always
# have both and can simplify the remaining code
if ($PSCmdlet.ParameterSetName -eq 'ServiceName')
{
$Service = Get-Service -Name $Name -ErrorAction Ignore
}
else
{
$Name = $Service.Name
}
# get the launch command for this service
$pathName = $hash.$Name.PathName
# look if the parameter "-k" was specified, followed by the service group name
# we are after (uses RegEx)
$groupName = if($pathName -match '-k\s+(\w+)')
{
$matches[1]
}
if ($PassThru)
{
# take the service and add the process ID
$Service |
Add-Member -MemberType NoteProperty -Name GroupName -Value $groupName -PassThru
}
else
{
$groupName
}
}
}
# NOW we are defining our web app
# thanks to our modular functions, this part is short and simple to
# understand and modify
New-UDApp -Content {
# 1.) prepare the data to show
# retrieve all services (make sure to catch any error)
$data = Get-Service -ErrorAction Ignore |
# ...add the service process ID...
Get-ServiceProcessId -PassThru |
# ...take only services that have a process ID...
Where-Object ProcessId |
# ...add the process name for the process ID...
Get-ProcessNameById -PassThru |
# ...add the service group name so we can differentiate svchost...
Get-ServiceGroupName -PassThru |
# ...sort by service displayname...
Sort-Object -Property DisplayName |
# select the properties to display in the table
Select-Object -Property DisplayName, Name, ProcessName, GroupName, ProcessId
# 2.) output it using one of the many available UI components, i.e. as a table
New-UDTable -Data $data
}
Al principio, encontrará las tres funciones que creamos antes. En una parte posterior de esta serie, moveremos estas funciones fuera del código principal y las colocaremos en módulos externos, pero por ahora, todo está definido en el propio código para no tener dependencias.
A continuación, los datos recuperados se muestran mediante el mismo New-UDTable que usó anteriormente. Cuando haga clic en «View App», su código se ejecuta, se recupera la información de servicios y se le presenta una tabla con los datos.
Todo lo que necesita es la URL de esta aplicación web: http://localhost:5000/list-services/Home. Podría guardarla como un enlace en su escritorio o añadirla a favoritos en su navegador. Siempre que necesite información de servicios, ya no tendrá que buscar scripts y ejecutarlos en una consola.
Y cuando asimile esto, verá las enormes posibilidades: puede crear sus propios paneles personales. Pero con una licencia corporativa y configurando PSU con privilegios de administrador, también podría alojar sus scripts como un servicio, ofreciendo acceso basado en navegador con control de seguridad granular a cualquier persona de su organización.
Llegaremos a eso a lo largo de esta serie. Por ahora, ha visto cómo configurar PSU y crear su primera aplicación web propia con PSU. En la siguiente parte, volveremos a empezar con código de PowerShell y luego ampliaremos el panel con nuevos controles y más funciones. También explicaremos por qué la tabla de servicios que acaba de crear muestra las columnas en un orden algo inesperado, y cómo corregirlo.

Tobias Weltner
Aleksandar Nikolić
Adam Driscoll

Steven Lafortune