开发者

Application (service) self-upgrade?

I'm developing a .NET4-based application in C# which runs as a windows service.

I'd like this application to be able to upgrade itself when instructed to do so by a web-service that it connects to periodically. Is there an accepted way to accomplish this? Is it even possible?

The way I'm thinking of it is something like this:

  1. The windows service (.exe) code downloads its replacement and supporting DLLs as a zip and extracts them to a temporary directory. The zip also includes a small "upgrader" executable or script.
  2. The service forks a child process to run the upgrader, passing the destination directory and any other required information on the command-line
  3. The service shuts down
  4. The upgrader process waits for the service to stop completely, then moves the necessary files (new .exe, DLLs) into the final installation directory, replacing the old file(s)
  5. The upgrader restarts the windows service, which spawns the (upgraded) .exe and quits once it starts

Would this work? You may detect from my terminology and approach that I'm from a UNIX background and not a windows background. I've made this approach work on UNIX, but I have no idea what sort of windows gotchas may exist...

UPDATE: My main motivation for this question is around the technical feasibility of a self-updating .NET application (how to do an in-place replacement of .DLLs, etc). As pointed out in开发者_Go百科 the comments, there are a host of other considerations involved in implementing a feature like this, in particular security concerns around verifying the new software components being applied are in fact legitimate. These are also important, but not specific to .NET or Windows (imo). Comments on these areas are welcome of course, but they are not my primary concern at this time...


Your approach is certainly reasonable, but unless you run under the LocalSystem account (not recommended!) you don't have permissions to write to your application folder or to start/stop your own service. Even if you run under the user's account, you might have problems because of UAC although I am not sure of this. Anyway running under the user's account is not good since it requires the user to enter the account password and has additional complications arising from password change, lockout etc. You will have to set ACLs on both from your installer, which will have to run elevated anyway to install a service.


You ought to take a look at this from Phil Haack, hot of the presses from earlier this week. I don't think it's exactly what you're looking for, but it might save you some time. NuGet is good times in any case.


This could be done using ClickOnce, but maybe not quite to the extent you want.

Take a look at this class

Imports System.Deployment.Application
Imports System.ComponentModel

Public Class UpdateChecker

    Public Enum UpdateType
        Automatic
        Manual
    End Enum

    Private Shared MyInstance As UpdateChecker
    Public Shared ReadOnly Property Current() As UpdateChecker
        Get
            If MyInstance Is Nothing Then
                MyInstance = New UpdateChecker
            End If
            Return MyInstance
        End Get
    End Property

    Private WithEvents CurrDeployment As ApplicationDeployment
    Private CurrType As UpdateType
    Private _checking As Boolean = False
    Private _lastErrorSentOnCheck As DateTime?

    Public ReadOnly Property LastUpdateCheck() As DateTime?
        Get
            If CurrDeployment IsNot Nothing Then
                Return CurrDeployment.TimeOfLastUpdateCheck
            End If
            Return Nothing
        End Get
    End Property

    Public Sub CheckAsync(ByVal checkType As UpdateType)
        Try
            Dim show As Boolean = (checkType = UpdateType.Manual)
            If ApplicationDeployment.IsNetworkDeployed AndAlso _
               Not WindowActive(show) AndAlso Not _checking AndAlso _
               (checkType = UpdateType.Manual OrElse Not LastUpdateCheck.HasValue OrElse LastUpdateCheck.Value.AddMinutes(60) <= Date.UtcNow) Then

                _checking = True

                CurrDeployment = ApplicationDeployment.CurrentDeployment
                CurrType = checkType

                Dim bw As New BackgroundWorker
                AddHandler bw.RunWorkerCompleted, AddressOf CurrDeployment_CheckForUpdateCompleted
                AddHandler bw.DoWork, AddressOf StartAsync

                If CurrType = UpdateType.Manual Then ShowWindow()

                bw.RunWorkerAsync()
            ElseIf checkType = UpdateType.Manual AndAlso _checking Then
                CurrType = checkType
                WindowActive(True)
            ElseIf checkType = UpdateType.Manual AndAlso Not ApplicationDeployment.IsNetworkDeployed Then
                MessageBox.Show(MainForm, "Cannot check for updates.", "Update", MessageBoxButtons.OK, MessageBoxIcon.Information)
            End If
        Catch ex As Exception
            If Not _lastErrorSentOnCheck.HasValue OrElse _lastErrorSentOnCheck.Value.AddHours(1) <= Now Then
                _lastErrorSentOnCheck = Now
                My.Application.LogError(ex, New StringPair("Update Check", checkType.ToString))
            End If
        End Try
    End Sub

    Private Sub StartAsync(ByVal sender As Object, ByVal e As DoWorkEventArgs)
        e.Result = CurrDeployment.CheckForDetailedUpdate
    End Sub

    Private Sub ShowWindow()
        My.Forms.frmUpdates.MdiParent = MainForm
        AddHandler My.Forms.frmUpdates.FormClosing, AddressOf frmUpdates_FormClosing
        My.Forms.frmUpdates.Show()
    End Sub

    Protected Sub frmUpdates_FormClosing(ByVal sender As Object, ByVal e As Windows.Forms.FormClosingEventArgs)
        My.Forms.frmUpdates = Nothing
    End Sub

    Private Function WindowActive(ByVal onTop As Boolean) As Boolean
        If Not My.Forms.frmUpdates Is Nothing Then
            If Not My.Forms.frmUpdates.Visible AndAlso onTop Then
                My.Forms.frmUpdates.MdiParent = MainForm
                My.Forms.frmUpdates.Show()
            ElseIf onTop Then
                My.Forms.frmUpdates.Activate()
            End If
            Return True
        End If
        Return False
    End Function

    Private Sub CurrDeployment_CheckForUpdateCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
        If MainForm.InvokeRequired Then
            MainForm.Invoke(New RunWorkerCompletedEventHandler(AddressOf CurrDeployment_CheckForUpdateCompleted), sender, e)
        Else
            If e.Error IsNot Nothing Then
                If WindowActive(True) Then My.Forms.frmUpdates.ShowError("Please try again later.")

                If Not _lastErrorSentOnCheck.HasValue OrElse _lastErrorSentOnCheck.Value.AddHours(1) <= Now Then
                    _lastErrorSentOnCheck = Now
                    My.Application.LogError(e.Error, New StringPair("Update Check Async", CurrType.ToString))
                End If
            Else
                Dim updateInfo As UpdateCheckInfo = DirectCast(e.Result, UpdateCheckInfo)
                Select Case CurrType
                    Case UpdateType.Manual
                        If WindowActive(False) Then My.Forms.frmUpdates.ShowCheckComplete(updateInfo)
                    Case UpdateType.Automatic
                        If updateInfo.UpdateAvailable Then
                            If Not WindowActive(True) Then ShowWindow()
                            My.Forms.frmUpdates.ShowCheckComplete(updateInfo)
                        End If
                End Select
            End If
            _checking = False
            End If

        DirectCast(sender, BackgroundWorker).Dispose()
    End Sub

    Public Sub UpdateAsync()
        If ApplicationDeployment.IsNetworkDeployed Then
            CurrDeployment = ApplicationDeployment.CurrentDeployment

            Dim bw As New BackgroundWorker
            AddHandler bw.RunWorkerCompleted, AddressOf CurrDeployment_UpdateCompleted
            AddHandler bw.DoWork, AddressOf StartUpdateAsync

            My.Forms.frmUpdates.ShowUpdateStart()

            bw.RunWorkerAsync()
        End If
    End Sub

    Public Sub StartUpdateAsync()
        CurrDeployment.Update()
    End Sub

    Private Sub CurrDeployment_UpdateCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles CurrDeployment.UpdateCompleted
        If MainForm.InvokeRequired Then
            MainForm.Invoke(New AsyncCompletedEventHandler(AddressOf CurrDeployment_UpdateCompleted), sender, e)
        Else
            If e.Error IsNot Nothing Then
                If WindowActive(True) Then My.Forms.frmUpdates.ShowError("Please try again later or close and re-open the application to automatically retrieve updates.")
                My.Application.LogError(e.Error, New StringPair("Update Async", CurrType.ToString))
            Else
                If WindowActive(True) Then My.Forms.frmUpdates.ShowUpdateComplete()
            End If
        End If
    End Sub
End Class

Here's the code to check if a new update is required. you'd run this on a timer, maybe every 5 minutes

 UpdateChecker.Current.CheckAsync(UpdateChecker.UpdateType.Automatic)

Then here's the code to download the update.

 UpdateChecker.Current.UpdateAsync()

The user would have to quit and start the application to get the new version or after the update completes you could also use Application.Restart to restart the application

This wouldn't update when the program isn't running, but with ClickOnce you can have it check for updates when the program is launched.


If the application is deployed inside a private network (intranet) you might consider using BITS. See this MSDN article.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜