/// <summary> /// Tests the sync logic with custom <see cref="AppList"/>s. /// </summary> /// <param name="resetMode">The <see cref="SyncResetMode"/> to pass to <see cref="SyncIntegrationManager.Sync"/>.</param> /// <param name="appListLocal">The current local <see cref="AppList"/>.</param> /// <param name="appListLast">The state of the <see cref="AppList"/> after the last successful sync.</param> /// <param name="appListServer">The current server-side <see cref="AppList"/>.</param> private void TestSync(SyncResetMode resetMode, AppList appListLocal, AppList appListLast, AppList appListServer) { appListLocal.SaveXml(_appListPath); if (appListLast != null) appListLast.SaveXml(_appListPath + SyncIntegrationManager.AppListLastSyncSuffix); using (var stream = File.Create(_appListPath + ".zip")) appListServer.SaveXmlZip(stream); using (var appListServerFile = File.OpenRead(_appListPath + ".zip")) using (var syncServer = new MicroServer("app-list", appListServerFile)) { using (var integrationManager = new SyncIntegrationManager(_appListPath, new SyncServer {Uri = syncServer.ServerUri, Username = "******", Password = "******"}, interfaceId => new Feed(), new SilentTaskHandler())) integrationManager.Sync(resetMode); appListServer = AppList.LoadXmlZip(syncServer.FileContent); } appListLocal = XmlStorage.LoadXml<AppList>(_appListPath); appListLast = XmlStorage.LoadXml<AppList>(_appListPath + SyncIntegrationManager.AppListLastSyncSuffix); Assert.AreEqual(appListLocal, appListServer, "Server and local data should be equal after sync"); Assert.AreEqual(appListLocal, appListLast, "Last sync snapshot and local data should be equal after sync"); }
/// <summary> /// Synchronize the <see cref="AppList"/> with the sync server and (un)apply <see cref="AccessPoint"/>s accordingly. /// </summary> /// <param name="resetMode">Controls how synchronization data is reset.</param> /// <exception cref="OperationCanceledException">The user canceled the task.</exception> /// <exception cref="InvalidDataException">A problem occurred while deserializing the XML data or the specified crypto key was wrong.</exception> /// <exception cref="KeyNotFoundException">An <see cref="AccessPoint"/> reference to a <see cref="Capability"/> is invalid.</exception> /// <exception cref="ConflictException">One or more new <see cref="AccessPoint"/> would cause a conflict with the existing <see cref="AccessPoint"/>s in <see cref="AppList"/>.</exception> /// <exception cref="WebException">A problem occurred while communicating with the sync server or while downloading additional data (such as icons).</exception> /// <exception cref="IOException">A problem occurs while writing to the filesystem or registry.</exception> /// <exception cref="UnauthorizedAccessException">Write access to the filesystem or registry is not permitted.</exception> public void Sync(SyncResetMode resetMode = SyncResetMode.None) { using var webClient = new WebClientTimeout { Credentials = new NetworkCredential(Config.SyncServerUsername, Config.SyncServerPassword), CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore) }; var uri = new Uri(_syncServer, new Uri(MachineWide ? "app-list-machine" : "app-list", UriKind.Relative)); ExceptionUtils.Retry<SyncRaceException>(delegate { Handler.CancellationToken.ThrowIfCancellationRequested(); // ReSharper disable AccessToDisposedClosure var data = DownloadAppList(webClient, uri, resetMode); HandleDownloadedAppList(data, resetMode); UploadAppList(webClient, uri, resetMode); // ReSharper restore AccessToDisposedClosure }, maxRetries: 3); // Save reference point for future syncs AppList.SaveXml(AppListPath + AppListLastSyncSuffix); Handler.CancellationToken.ThrowIfCancellationRequested(); }
/// <summary> /// Tests the sync logic with custom <see cref="AppList"/>s. /// </summary> /// <param name="resetMode">The <see cref="SyncResetMode"/> to pass to <see cref="SyncIntegrationManager.Sync"/>.</param> /// <param name="appListLocal">The current local <see cref="AppList"/>.</param> /// <param name="appListLast">The state of the <see cref="AppList"/> after the last successful sync.</param> /// <param name="appListServer">The current server-side <see cref="AppList"/>.</param> private static void TestSync(SyncResetMode resetMode, AppList appListLocal, AppList?appListLast, AppList appListServer) { string appListLocalPath = AppList.GetDefaultPath(); appListLocal.SaveXml(appListLocalPath); appListLast?.SaveXml(appListLocalPath + SyncIntegrationManager.AppListLastSyncSuffix); using var appListServerPath = new TemporaryFile("0install-test-applist"); { using (var stream = File.Create(appListServerPath)) appListServer.SaveXmlZip(stream, CryptoKey); using (var appListServerFile = File.OpenRead(appListServerPath)) { using var syncServer = new MicroServer("app-list", appListServerFile); var config = new Config { SyncServer = new(syncServer.ServerUri), SyncServerUsername = "******", SyncServerPassword = "******", SyncCryptoKey = CryptoKey }; using (var integrationManager = new SyncIntegrationManager(config, _ => new Feed(), new SilentTaskHandler())) integrationManager.Sync(resetMode); appListServer = AppList.LoadXmlZip(syncServer.FileContent, CryptoKey); } } appListLocal = XmlStorage.LoadXml <AppList>(appListLocalPath); appListLast = XmlStorage.LoadXml <AppList>(appListLocalPath + SyncIntegrationManager.AppListLastSyncSuffix); appListServer.Should().Be(appListLocal, because: "Server and local data should be equal after sync"); appListLast.Should().Be(appListLocal, because: "Last sync snapshot and local data should be equal after sync"); }
/// <inheritdoc/> protected override void Finish() { Log.Debug("Saving AppList to: " + AppListPath); // Retry to handle race conditions with read-only access to the file ExceptionUtils.Retry <IOException>(delegate { AppList.SaveXml(AppListPath); }); if (WindowsUtils.IsWindows) { WindowsUtils.NotifyAssocChanged(); // Notify Windows Explorer of changes WindowsUtils.BroadcastMessage(ChangedWindowMessageID); // Notify Zero Install GUIs of changes } }
/// <summary> /// Creates a new sync manager. Performs Mutex-based locking! /// </summary> /// <param name="config">Configuration for communicating with a sync server.</param> /// <param name="feedRetriever">Callback method used to retrieve additional <see cref="Feed"/>s on demand.</param> /// <param name="handler">A callback object used when the the user is to be informed about the progress of long-running operations such as downloads.</param> /// <param name="machineWide">Apply operations machine-wide instead of just for the current user.</param> /// <exception cref="IOException">A problem occurred while accessing the <see cref="AppList"/> file.</exception> /// <exception cref="UnauthorizedAccessException">Read or write access to the <see cref="AppList"/> file is not permitted or another desktop integration class is currently active.</exception> /// <exception cref="InvalidDataException">A problem occurred while deserializing the XML data.</exception> public SyncIntegrationManager(Config config, Converter<FeedUri, Feed> feedRetriever, ITaskHandler handler, bool machineWide = false) : base(config, handler, machineWide) { if (!config.IsSyncConfigured) throw new InvalidDataException(Resources.PleaseConfigSync); _syncServer = config.SyncServer.EnsureTrailingSlash(); _feedRetriever = feedRetriever ?? throw new ArgumentNullException(nameof(feedRetriever)); if (File.Exists(AppListPath + AppListLastSyncSuffix)) _appListLastSync = XmlStorage.LoadXml<AppList>(AppListPath + AppListLastSyncSuffix); else { _appListLastSync = new AppList(); _appListLastSync.SaveXml(AppListPath + AppListLastSyncSuffix); } }
/// <summary> /// Creates a new integration manager using the default <see cref="DesktopIntegration.AppList"/> (creating a new one if missing). Performs Mutex-based locking! /// </summary> /// <param name="config">User settings controlling network behaviour.</param> /// <param name="handler">A callback object used when the the user is to be informed about the progress of long-running operations such as downloads.</param> /// <param name="machineWide">Apply operations machine-wide instead of just for the current user.</param> /// <exception cref="IOException">A problem occurred while accessing the <see cref="AppList"/> file.</exception> /// <exception cref="UnauthorizedAccessException">Read or write access to the <see cref="AppList"/> file is not permitted or another desktop integration class is currently active.</exception> /// <exception cref="InvalidDataException">A problem occurred while deserializing the XML data.</exception> public IntegrationManager(Config config, ITaskHandler handler, bool machineWide = false) : base(handler, machineWide) { Config = config ?? throw new ArgumentNullException(nameof(config)); try { AcquireMutex(); } catch (TimeoutException) { throw new UnauthorizedAccessException(Resources.IntegrationMutex); } try { AppListPath = AppList.GetDefaultPath(machineWide); if (File.Exists(AppListPath)) { Log.Debug("Loading AppList for IntegrationManager from: " + AppListPath); AppList = XmlStorage.LoadXml <AppList>(AppListPath); } else { Log.Debug("Creating new AppList for IntegrationManager: " + AppListPath); AppList = new AppList(); AppList.SaveXml(AppListPath); } } #region Error handling catch { // Avoid abandoned mutexes Dispose(); throw; } #endregion }
/// <summary> /// Creates a new integration manager using the default <see cref="DesktopIntegration.AppList"/> (creating a new one if missing). Performs Mutex-based locking. /// </summary> /// <param name="handler">A callback object used when the the user is to be informed about the progress of long-running operations such as downloads.</param> /// <param name="machineWide">Apply operations machine-wide instead of just for the current user.</param> /// <exception cref="IOException">A problem occurs while accessing the <see cref="AppList"/> file.</exception> /// <exception cref="UnauthorizedAccessException">Read or write access to the <see cref="AppList"/> file is not permitted or if another desktop integration class is currently active.</exception> /// <exception cref="InvalidDataException">A problem occurs while deserializing the XML data.</exception> public IntegrationManager([NotNull] ITaskHandler handler, bool machineWide = false) : base(handler) { #region Sanity checks if (handler == null) throw new ArgumentNullException("handler"); #endregion MachineWide = machineWide; _mutex = AquireMutex(); try { AppListPath = AppList.GetDefaultPath(machineWide); if (File.Exists(AppListPath)) { Log.Debug("Loading AppList for IntegrationManager from: " + AppListPath); AppList = XmlStorage.LoadXml<AppList>(AppListPath); } else { Log.Debug("Creating new AppList for IntegrationManager: " + AppListPath); AppList = new AppList(); AppList.SaveXml(AppListPath); } } #region Error handling catch { // Avoid abandoned mutexes _mutex.ReleaseMutex(); _mutex.Close(); throw; } #endregion }
/// <summary> /// Creates a new integration manager using the default <see cref="DesktopIntegration.AppList"/> (creating a new one if missing). Performs Mutex-based locking! /// </summary> /// <param name="handler">A callback object used when the the user is to be informed about the progress of long-running operations such as downloads.</param> /// <param name="machineWide">Apply operations machine-wide instead of just for the current user.</param> /// <exception cref="IOException">A problem occurs while accessing the <see cref="AppList"/> file.</exception> /// <exception cref="UnauthorizedAccessException">Read or write access to the <see cref="AppList"/> file is not permitted or another desktop integration class is currently active.</exception> /// <exception cref="InvalidDataException">A problem occurs while deserializing the XML data.</exception> public IntegrationManager([NotNull] ITaskHandler handler, bool machineWide = false) : base(handler, machineWide) { #region Sanity checks if (handler == null) throw new ArgumentNullException(nameof(handler)); #endregion try { AquireMutex(); } #region Error handling catch (UnauthorizedAccessException) { // Replace exception to add more context throw new UnauthorizedAccessException(Resources.IntegrationMutex); } #endregion try { AppListPath = AppList.GetDefaultPath(machineWide); if (File.Exists(AppListPath)) { Log.Debug("Loading AppList for IntegrationManager from: " + AppListPath); AppList = XmlStorage.LoadXml<AppList>(AppListPath); } else { Log.Debug("Creating new AppList for IntegrationManager: " + AppListPath); AppList = new AppList(); AppList.SaveXml(AppListPath); } } #region Error handling catch { // Avoid abandoned mutexes Dispose(); throw; } #endregion }
/// <summary> /// Creates a new sync manager for a custom <see cref="AppList"/> file. Used for testing. Uses no mutex! /// </summary> /// <param name="appListPath">The storage location of the <see cref="AppList"/> file.</param> /// <param name="server">Access information for the sync server.</param> /// <param name="feedRetriever">Callback method used to retrieve additional <see cref="Feed"/>s on demand.</param> /// <param name="handler">A callback object used when the the user is to be informed about the progress of long-running operations such as downloads.</param> /// <param name="machineWide">Apply operations machine-wide instead of just for the current user.</param> /// <exception cref="IOException">A problem occurs while accessing the <see cref="AppList"/> file.</exception> /// <exception cref="UnauthorizedAccessException">Read or write access to the <see cref="AppList"/> file is not permitted or another desktop integration class is currently active.</exception> /// <exception cref="InvalidDataException">A problem occurs while deserializing the XML data.</exception> public SyncIntegrationManager([NotNull] string appListPath, SyncServer server, [NotNull] Converter<FeedUri, Feed> feedRetriever, [NotNull] ITaskHandler handler, bool machineWide = false) : base(appListPath, handler, machineWide) { #region Sanity checks if (server.Uri == null) throw new ArgumentNullException(nameof(server)); if (feedRetriever == null) throw new ArgumentNullException(nameof(feedRetriever)); #endregion _server = server; _feedRetriever = feedRetriever; if (File.Exists(AppListPath + AppListLastSyncSuffix)) _appListLastSync = XmlStorage.LoadXml<AppList>(AppListPath + AppListLastSyncSuffix); else { _appListLastSync = new AppList(); _appListLastSync.SaveXml(AppListPath + AppListLastSyncSuffix); } }
private static void TestSaveLoad(AppList appList) { Assert.That(appList, Is.XmlSerializable); AppList appList2; using (var tempFile = new TemporaryFile("0install-unit-tests")) { // Write and read file appList.SaveXml(tempFile); appList2 = XmlStorage.LoadXml<AppList>(tempFile); } // Ensure data stayed the same appList2.Should().Be(appList, because: "Serialized objects should be equal."); appList2.GetHashCode().Should().Be(appList.GetHashCode(), because: "Serialized objects' hashes should be equal."); appList2.Should().NotBeSameAs(appList, because: "Serialized objects should not return the same reference."); }