Topshelf.Squirrel.Windows
Topshelf.Squirrel.Windows copied to clipboard
Service isn't updating
I'm trying to better understand the workings of this rather intriguing piece of code.
As it is, the service isn't updating. I've duplicated your configurations, e.g. SquirrelAwareApp
, double-check Releases path, etc.
I should note that the service is throwing an exception during stop:
Failed to stop service. System.InvalidOperationException: An unhandled exception was detected ---> System.Exception: You must dispose UpdateManager! at Squirrel.UpdateManager.Finalize() --- End of inner exception stack trace --- at Topshelf.Runtime.Windows.WindowsServiceHost.OnStop() at System.ServiceProcess.ServiceBase.DeferredStop()
Paul recommends keeping UpdateManager
around only for the duration of the actual updating action, as noted here. (At first I'd thought that this Disposing
error only occurred during Debug, but now I see that it's happening even during Release.)
- I see here that you're updating the app, but doesn't the Squirrel construct require a restart to apply an update?
- Is this the call that performs the actual update? How do we get that to run? Should we be hooking into the Squirrel events, since this is a
SquirrelAwareApp
? - Where does the code modify the service's registry entry to point to the new release? Does Topshelf handle that?
- What is the purpose of
WithOverlapping
?
That said, my test service isn't even acquiring the latest release from the distribution folder. I don't know whether that's related to the Disposing
error discussed previously, but I probably doubt it.
Nice work on this. I'd like to contribute, but I'm not very handy with C# I'm afraid.
UPDATE
It works now.
The problem was due to an incorrect Releases path. The value of the string variable was correct, but I failed to account for it being modified in the code. Once I got that fixed, the service updated as expected.
This answers the first three of my questions, however I'm still curious about the intended usage of WithOverlapping
. It appears to govern whether the service stays running during the update process, but I could be mistaken. Please advise.
Here's some updated code (partial credit to Nonobis) to fix the Disposing
bug, as well as add important cancellation support:
namespace Topshelf.Squirrel.Windows.Interfaces
{
public interface IUpdater
{
void Start();
void Cancel();
}
}
using System;
using System.Reflection;
using Topshelf.HostConfigurators;
using Topshelf.Squirrel.Windows.Builders;
using Topshelf.Squirrel.Windows.Interfaces;
namespace Topshelf.Squirrel.Windows
{
public class SquirreledHost
{
private readonly string serviceName;
private readonly string serviceDisplayName;
private readonly bool withOverlapping;
private readonly bool promtCredsWhileInstall;
private readonly ISelfUpdatableService selfUpdatableService;
private readonly IUpdater updater;
public SquirreledHost(
ISelfUpdatableService selfUpdatableService,
string serviceName = null,
string serviceDisplayName = null, IUpdater updater = null, bool withOverlapping = false, bool promtCredsWhileInstall = false)
{
var assemblyName = Assembly.GetEntryAssembly().GetName().Name;
this.serviceName = serviceName ?? assemblyName;
this.serviceDisplayName = serviceDisplayName ?? assemblyName;
this.selfUpdatableService = selfUpdatableService;
this.withOverlapping = withOverlapping;
this.promtCredsWhileInstall = promtCredsWhileInstall;
this.updater = updater;
}
public void ConfigureAndRun(ConfigureExt configureExt = null)
{
HostFactory.Run(configurator => { Configure(configurator); configureExt?.Invoke(configurator); });
}
public delegate void ConfigureExt(HostConfigurator config);
private void Configure(HostConfigurator config)
{
config.Service<ISelfUpdatableService>(service =>
{
service.ConstructUsing(settings => selfUpdatableService);
service.WhenStarted((s, hostControl) =>
{
s.Start();
return true;
});
service.AfterStartingService(() => { updater?.Start(); });
service.BeforeStoppingService(() => { updater?.Cancel(); });
service.WhenStopped(s => { s.Stop(); });
});
config.SetServiceName(serviceName);
config.SetDisplayName(serviceDisplayName);
config.StartAutomatically();
config.EnableShutdown();
if (promtCredsWhileInstall)
{
config.RunAsFirstPrompt();
}
else
{
config.RunAsLocalSystem();
}
config.AddCommandLineSwitch("squirrel", _ => { });
config.AddCommandLineDefinition("firstrun", _ => Environment.Exit(0));
config.AddCommandLineDefinition("obsolete", _ => Environment.Exit(0));
config.AddCommandLineDefinition("updated", version => { config.UseHostBuilder((env, settings) => new UpdateHostBuilder(env, settings, version, withOverlapping)); });
config.AddCommandLineDefinition("install", version => { config.UseHostBuilder((env, settings) => new InstallAndStartHostBuilder(env, settings, version)); });
config.AddCommandLineDefinition("uninstall", _ => { config.UseHostBuilder((env, settings) => new StopAndUninstallHostBuilder(env, settings)); });
}
}
}
using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using NuGet;
using Squirrel;
using Topshelf.Squirrel.Windows.Interfaces;
namespace Topshelf.Squirrel.Windows
{
public class RepeatedTimeUpdater : IUpdater
{
private TimeSpan checkUpdatePeriod = TimeSpan.FromSeconds(30);
private readonly IUpdateManager updateManager;
private string curVersion;
private string updateSource;
private string appName;
private CancellationTokenSource tokenSource;
private Task updaterTask;
/// <summary>
/// Задать время между проверками доступности обновлений. По умолчанию 30 секунд.
/// </summary>
/// <param name="checkSpan"></param>
/// <returns></returns>
public RepeatedTimeUpdater SetCheckUpdatePeriod(TimeSpan checkSpan)
{
checkUpdatePeriod = checkSpan;
return this;
}
[Obsolete("Will be removed")]
public RepeatedTimeUpdater(IUpdateManager updateManager)
{
if (!Environment.UserInteractive)
{
if (updateManager == null)
throw new Exception("Update manager can not be null");
}
curVersion = Assembly.GetEntryAssembly().GetName().Version.ToString();
this.updateManager = updateManager;
}
public RepeatedTimeUpdater(string pUrlOrPath, string pApplicationName = null)
{
curVersion = Assembly.GetEntryAssembly().GetName().Version.ToString();
updateSource = pUrlOrPath;
appName = pApplicationName;
tokenSource = new CancellationTokenSource();
}
/// <summary>
/// Метод который проверяет обновления
/// </summary>
public void Start()
{
if (!Environment.UserInteractive)
{
updaterTask = Task.Run(Update);
updaterTask.ConfigureAwait(false);
}
}
public void Cancel()
{
if (!Environment.UserInteractive)
{
tokenSource.Cancel();
updaterTask.Wait();
}
}
[Obsolete("Will be removed")]
private async Task OldUpdate()
{
if (updateManager == null)
throw new Exception("Update manager can not be null");
Trace.TraceInformation("Automatic-renewal was launched ({0})", curVersion);
{
while (true)
{
await Task.Delay(checkUpdatePeriod);
try
{
//Проверяем наличие новой версии
var update = await updateManager.CheckForUpdate();
try
{
var oldVersion = update.CurrentlyInstalledVersion?.Version ?? new SemanticVersion(0, 0, 0, 0);
var newVersion = update.FutureReleaseEntry.Version;
if (oldVersion < newVersion)
{
Trace.TraceInformation("Found a new version: {0}", newVersion);
//Скачиваем новую версию
await updateManager.DownloadReleases(update.ReleasesToApply);
//Распаковываем новую версию
await updateManager.ApplyReleases(update);
}
}
catch (Exception ex)
{
Trace.TraceError("Error on update ({0}): {1}", curVersion, ex);
}
}
catch (Exception ex)
{
Trace.TraceError("Error on check for update ({0}): {1}", curVersion, ex);
}
}
}
}
private async Task Update()
{
Trace.TraceInformation("Automatic-renewal was launched ({0})", curVersion);
while (!tokenSource.Token.IsCancellationRequested)
{
await Task.Delay(checkUpdatePeriod);
try
{
using (var upManager = new UpdateManager(updateSource, appName))
{
// Check for update
var update = await upManager.CheckForUpdate();
var oldVersion = update.CurrentlyInstalledVersion?.Version ?? new SemanticVersion(0, 0, 0, 0);
Trace.TraceInformation("Installed version: {0}", oldVersion);
var newVersion = update.FutureReleaseEntry?.Version;
if (newVersion != null && oldVersion < newVersion)
{
Trace.TraceInformation("Found a new version: {0}", newVersion);
// Downlaod Release
await upManager.DownloadReleases(update.ReleasesToApply);
// Apply Release
await upManager.ApplyReleases(update);
}
}
}
catch (Exception ex)
{
Trace.TraceError("Error on check for update ({0}): {1}", curVersion, ex);
}
}
}
}
}