VB .NET - Saindo do Windows e Fazendo o Logoff


Em certas ocasiões, após realizar determinada atualização, é necessário que o sistema seja inicializado e o usuário faça o log-off para realizar um novo login e assim acessar o sistema atualizado.

Este artigo vai mostrar como utilizar a API do Windows para sair do Windows e realizar o log-off do usuário.

A função ExitWindowsEx permite que uma mensagem seja enviada a todos os processos em execução para o usuário atual, solicitando que eles terminem de forma a permitir que o computador seja desligado, reiniciado ou fique em preparação para o usuário atual ser desconectado.

Esta função desconecta o usuário interativo, desliga o sistema, ou desliga e reinicia o sistema. Ela envia a mensagem WM_QUERYENDSESSION para todas as aplicações para determinar se elas podem ser encerradas.

A chamada da API é usada para terminar todos os processos do usuário atual. Quando o usuário que executa a função é o usuário interativo, isso também pode fazer com que o sistema automaticamente desligue ou reinicie.

Quando o comando é chamado de um serviço que está sendo executado sob as credenciais de um usuário diferente ou dentro de uma sessão do Terminal Services, a função pode parar com sucesso todos os processos atuais sem iniciar um desligamento do servidor físico. (Nesta situação, se um desligamento é necessária a API InitiateSystemShutdownEx pode ser utilizada.)

Criando um projeto VB .NET

Vamos então criar um programa VB .NET que demonstra como desligar e/ou reiniciar o sistema via código.

Abra o Visual Basic 2010 Express Edition e crie um novo projeto do tipo Windows Forms Applcaction chamado "ShutdownVBNET".

No formulário padrão form1.vb adicione três botões de comando conforme o leiaute abaixo:

Controles Button:
  • btnShutDown
  • btnReiniciar
  • btnLogoff

O .NET Framework não provê nativamente qualquer meio de desligar o sistema. Em vez disso, você deve usar uma função da API do Windows para esta tarefa. A primeira coisa que precisamos fazer é referenciar o namespace InteropServices para que possamos acessar as funções da API do Windows. Para fazer isso, adicione uma nova diretiva no início do formulário

Imports System.Runtime.InteropServices

Devemos também declarar o seguinte namespace:

Imports System.ComponentModel

Uma vez que a referência ao namespace InteropServices foi adicionada, podemos declarar a função da API como uma função estática de uma classe ou no formulário conforme a seguir:

<DllImport("user32.dll", SetLastError:=True)> _
    Private Shared Function ExitWindowsEx(ByVal uFlags As UInteger, ByVal dwReason As UInteger) As Integer
End Function

A função ExitWindowsEx utiliza dois parâmetros :

  1. O primeiro parâmetro, uFlags, determina a ação que deve ser tomada quando a função é chamada. O valor inteiro a ser usado é determinado pela combinação de um ou mais valores de flags definidos em uma enumeração. Para tornar o código mais fácil de ler, vamos definir então uma nova enumeração contendo os valores fundamentais para os nossos propósitos.

Vamos incluir o código abaixo que define uma enumeração com os flags usados:

    Enum ExitFlags
        Logoff = 0
        Shutdown = 1
        Reboot = 2
        Force = 4
        PowerOff = 8
        ForceIfHung = 16
    End Enum
  1. O segundo parâmetro da função ExitWindowsEx é usado para definir o motivo pelo qual o sistema foi desligado. Existem oito principais motivos para o Windows ser encerrado ou reiniciado. Cada um deles pode ser modificado usando uma combinação de motivos. Há várias dezenas de códigos de motivos disponíveis, mas vamos definir os principais motivos em uma segunda enumeração conforme abaixo:
    Enum Motivo As UInteger
    ApplicationIssue = &H40000
    HardwareIssue = &H10000
    SoftwareIssue = &H30000
    PlannedShutdown = &H80000000UI
    End Enum

Elevando os Previlégios

Todos os programas são executados com um conjunto de privilégios que habilita e desabilita a funcionalidade do Windows. Por motivos de segurança os privilégios previstos em um programa devem ser o mínimo necessário para garantir a execução correta. Por este motivo, a capacidade de desligar o Windows não é fornecido como padrão para uma aplicação. O privilégio deve ser solicitado antes do comando de desligamento estar disponível.

Elevar os privilégios do programa para incluir o requerido direito de ShutDown usa uma série de chamadas de API. Para isso as seguintes funções são utilizadas:

Vamos criar um novo método para o nosso exemplo que atribui o privilégio para o processo atual antes de tentar executar qualquer procedimento de shutdown.

Antes de criar o método temos que declarar as 4 chamadas à API. Essas chamadas usam alguns valores constantes e uma estrutura.

Abaixo temos estas declarações no formulário form1.vb:

Const PrivilegeEnabled As Integer = &H2
Const TokenQuery As Integer = &H8
Const AdjustPrivileges As Integer = &H20
Const ShutdownPrivilege As String = "SeShutdownPrivilege"

<StructLayout(LayoutKind.Sequential, Pack := 1)> _
Friend Structure TokenPrivileges
	Public PrivilegeCount As Integer
	Public Luid As Long
	Public Attributes As Integer
End Structure

<DllImport("kernel32.dll")> _
Friend Shared Function GetCurrentProcess() As IntPtr
End Function

<DllImport("advapi32.dll", SetLastError := True)> _
Friend Shared Function OpenProcessToken(processHandle As IntPtr, desiredAccess As Integer, ByRef tokenHandle As IntPtr) As Integer
End Function

<DllImport("advapi32.dll", SetLastError := True)> _
Friend Shared Function LookupPrivilegeValue(systemName As String, name As String, ByRef luid As Long) As Integer
End Function

<DllImport("advapi32.dll", SetLastError := True)> _
Friend Shared Function AdjustTokenPrivileges(tokenHandle As IntPtr, disableAllPrivileges As Boolean, ByRef newState As TokenPrivileges, bufferLength As Integer, 
  previousState As IntPtr, length As IntPtr) As Integer
End Function

Feitas as declarações o novo método pode ser adicionado ao código. Este método usa as 4 funções da API para identificar o processo atual, recuperar os privilégios atuais, identificar o privilégio de desligamento e concedê-lo para o usuário. Adicione o seguinte método no código do formulário:

 Private Sub ElevarPrevilegios()
        Dim currentProcess As IntPtr = GetCurrentProcess()
        Dim tokenHandle As IntPtr = IntPtr.Zero

        Dim result As Integer = OpenProcessToken(currentProcess, AdjustPrivileges Or TokenQuery, tokenHandle)
        If result = 0 Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If

        Dim tokenPrivileges As TokenPrivileges
        tokenPrivileges.PrivilegeCount = 1
        tokenPrivileges.Luid = 0
        tokenPrivileges.Attributes = PrivilegeEnabled

        result = LookupPrivilegeValue(Nothing, ShutdownPrivilege, tokenPrivileges.Luid)
        If result = 0 Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If

        result = AdjustTokenPrivileges(tokenHandle, False, tokenPrivileges, 0, IntPtr.Zero, IntPtr.Zero)
        If result = 0 Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If
    End Sub

Saindo do Windows

Com a API referenciada, os códigos das enumerações flag, os motivos disponíveis e um método que eleva os privilégios do programa para permitir shutdowns definidos, podemos adicionar a funcionalidade aos botões de comando do formulário para demonstrar o uso do método ExitWindowsEx.

Quando a função ExitWindowsEx é chamada, ela retorna um valor inteiro indicando o sucesso ou fracasso. Se a chamada falhar, o valor de retorno é zero e os detalhes do erro são armazenados. O erro pode ser recuperado usando o método GetLastWin32Error da classe Marshal. Embora este método retorne um número inteiro, os detalhes da exceção podem ser vistos lançando a exceção Win32Exception e passando o número de erro como um parâmetro.

Para adicionar o shutdown e o código de detecção de erros para o botão ShutDown, adicione o seguinte código ao evento Click do botão. Esse exemplo combina dois códigos de motivo para indicar ao sistema operacional que o sistema foi desligado devido a um problema de hardware, mas que isso foi planejado:

 Private Sub btnShutDown_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnShutDown.Click
        ElevarPrevilegios()

        Dim result As Integer = ExitWindowsEx(CUInt(ExitFlags.Shutdown Or ExitFlags.PowerOff), CUInt(Motivo.HardwareIssue Or Motivo.PlannedShutdown))
        If result = 0 Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If
    End Sub

Para testar podemos executar o projeto e clicar no botão ShutDown.(Lembre-se de salvar o seu trabalho)

Forçando o ShutDown

O procedimento de desligamento descrito acima envia uma mensagem para todos os processos do usuário atual, solicitando que eles sejam fechados. Isto é semelhante a fechar o Windows através do menu Iniciar. Cada programa que está sendo executado irá fechar normalmente, dando ao usuário a opção de salvar qualquer trabalho aberto sempre que adequado. Se qualquer um dos softwares em execução não fechar dentro de um período de tempo limite, ao usuário é dada a opção de encerrar os programas à força ou cancelar o procedimento de desligamento.

Em alguns casos de emergência, como quando os programas travam, você pode querer forçar o sistema a fechar todos os processos ativos. Ao incluir a flag Force, todos os processos são obrigados a fechar e ao usuário não é dada a opção de cancelar o processo. A Flag ForceIfHung também pode ser usada para forçar as aplicações a encerrar se elas não responderem dentro do tempo limite. Qualquer opção deve ser usada com cuidado, pois elas podem fechar aplicativos sem dar ao usuário a opção de salvar seu trabalho em primeiro lugar.

Para forçar um desligamento, ajuste o código do evento Click do botão ShutDown conforme abaixo:

  Private Sub btnShutDown_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnShutDown.Click
        ElevarPrevilegios()

        Dim result As Integer = ExitWindowsEx(CUInt(ExitFlags.Shutdown Or ExitFlags.PowerOff Or ExitFlags.Force), CUInt(Motivo.HardwareIssue Or Motivo.PlannedShutdown))
        If result = 0 Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If
    End Sub

Reiniciando o Windows

Se a sua aplicação fez alterações na configuração do Windows, você pode exigir que o sistema operacional seja reiniciado em vez de simplesmente ser desligado.

Isto é feito através da modificação do parâmetro flag da função ExitWindowsEx, de modo que ela use a flag Reboot, em vez das diversas flags ShutDown. A flag Force pode também ser combinada com a flag Reboot.

Para adicionar o código de reinício para o aplicativo exemplo, inclua o código abaixo no evento CLick do botão Reiniciar:

 Private Sub btnReiniciar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnReiniciar.Click
        ElevarPrevilegios()

        Dim result As Integer = ExitWindowsEx(CUInt(ExitFlags.Reboot), CUInt(Motivo.SoftwareIssue Or Motivo.PlannedShutdown))
        If result = 0 Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If
    End Sub

Fazendo o Logoff

O último botão do nosso exemplo será usado apenas para dar o log-off no usuário do Windows. Isto é útil para alterações de configuração menores onde uma reinicialização total não é necessário. Mais uma vez, as flags da função ExitWindowsEx são a única alteração necessária.

Para completar o programa de exemplo, adicionar o código abaixo no evento Click do botão Logoff:

 Private Sub btnLogoff_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnLogoff.Click
        ElevarPrevilegios()

        Dim result As Integer = ExitWindowsEx(CUInt(ExitFlags.Logoff), CUInt(Motivo.ApplicationIssue Or Motivo.PlannedShutdown))
        If result = 0 Then
            Throw New Win32Exception(Marshal.GetLastWin32Error())
        End If
    End Sub

Assim completamos as funcionalidades que permitem a nossa aplicação VB .NET utilizar a função ExitWindowsEx para realizar a reinicialização, o desligamento e o log-off do usuário.

Pegue o projeto completo aqui: ShutDownVBNET.zip

1Jo 2:1 Meus filhinhos, estas coisas vos escrevo, para que não pequeis; mas, se alguém pecar, temos um Advogado para com o Pai, Jesus Cristo, o justo.

1Jo 2:2 E ele é a propiciação pelos nossos pecados, e não somente pelos nossos, mas também pelos de todo o mundo.

1Jo 2:3 E nisto sabemos que o conhecemos; se guardamos os seus mandamentos.

Referências:


José Carlos Macoratti