using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Security; using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; using Jackett.Common.Models.Config; using Jackett.Common.Models.GitHub; using Jackett.Common.Services.Interfaces; using Jackett.Common.Utils; using Jackett.Common.Utils.Clients; using Newtonsoft.Json; using NLog; namespace Jackett.Common.Services { public class UpdateService : IUpdateService { private readonly Logger logger; private readonly WebClient client; private readonly ManualResetEvent locker = new ManualResetEvent(false); private readonly ITrayLockService lockService; private readonly IServiceConfigService windowsService; private readonly IFilePermissionService filePermissionService; private readonly ServerConfig serverConfig; private bool forceUpdateCheck; // false by default private Variants.JackettVariant variant; public UpdateService(Logger l, WebClient c, ITrayLockService ls, IServiceConfigService ws, IFilePermissionService fps, ServerConfig sc) { logger = l; client = c; lockService = ls; windowsService = ws; serverConfig = sc; filePermissionService = fps; variant = new Variants().GetVariant(); } private string ExePath() { // Use EscapedCodeBase to avoid Uri reserved characters from causing bugs // https://stackoverflow.com/questions/896572 var location = new Uri(Assembly.GetEntryAssembly().GetName().EscapedCodeBase); // Use LocalPath instead of AbsolutePath to avoid needing to unescape Uri format. return new FileInfo(location.LocalPath).FullName; } public void StartUpdateChecker() => Task.Factory.StartNew(UpdateWorkerThread); public void CheckForUpdatesNow() { forceUpdateCheck = true; locker.Set(); } private async void UpdateWorkerThread() { var delayHours = 1; // first check after 1 hour (for users not running jackett 24/7) while (true) { locker.WaitOne((int)new TimeSpan(delayHours, 0, 0).TotalMilliseconds); locker.Reset(); await CheckForUpdates(); delayHours = 24; // following checks only once/24 hours } } private bool AcceptCert(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) => true; private async Task CheckForUpdates() { logger.Info("Checking for updates... Jackett variant: " + variant); if (serverConfig.RuntimeSettings.NoUpdates) { logger.Info("Updates are disabled via --NoUpdates."); return; } if (serverConfig.UpdateDisabled && !forceUpdateCheck) { logger.Info("Skipping update check as it is disabled."); return; } forceUpdateCheck = false; // Used when updates are disabled and the user click update button if (Debugger.IsAttached) { logger.Info("Skipping checking for new releases as the debugger is attached."); return; } var currentVersion = $"v{GetCurrentVersion()}"; if (currentVersion == "v0.0.0") { logger.Info("Skipping checking for new releases because Jackett is runing in IDE."); return; } var isWindows = Environment.OSVersion.Platform != PlatformID.Unix; var trayIsRunning = false; if (isWindows) { trayIsRunning = Process.GetProcessesByName("JackettTray").Length > 0; } try { var response = await client.GetString(new WebRequest() { Url = "https://api.github.com/repos/Jackett/Jackett/releases", Encoding = Encoding.UTF8, EmulateBrowser = false }); if (response.Status != System.Net.HttpStatusCode.OK) { logger.Error("Failed to get the release list: " + response.Status); } var releases = JsonConvert.DeserializeObject>(response.Content); if (!serverConfig.UpdatePrerelease) { releases = releases.Where(r => !r.Prerelease).ToList(); } if (releases.Count > 0) { var latestRelease = releases.OrderByDescending(o => o.Created_at).First(); if (latestRelease.Name != currentVersion) { logger.Info($"New release found. Current version: {currentVersion} New version: {latestRelease.Name}"); logger.Info($"Downloading release {latestRelease.Name} It could take a while..."); try { var tempDir = await DownloadRelease(latestRelease.Assets, isWindows, latestRelease.Name); // Copy updater var installDir = Path.GetDirectoryName(ExePath()); var updaterPath = GetUpdaterPath(tempDir); if (updaterPath != null) { StartUpdate(updaterPath, installDir, isWindows, serverConfig.RuntimeSettings.NoRestart, trayIsRunning); } } catch (Exception e) { logger.Error(e, "Error performing update."); } } else { logger.Info($"Jackett is already updated. Current version: {currentVersion}"); } } } catch (Exception e) { logger.Error(e, "Error checking for updates."); } finally { if (!isWindows) { System.Net.ServicePointManager.ServerCertificateValidationCallback -= AcceptCert; } } } private string GetUpdaterPath(string tempDirectory) => variant == Variants.JackettVariant.CoreMacOs || variant == Variants.JackettVariant.CoreLinuxAmdx64 || variant == Variants.JackettVariant.CoreLinuxArm32 || variant == Variants.JackettVariant.CoreLinuxArm64 ? Path.Combine(tempDirectory, "Jackett", "JackettUpdater") : Path.Combine(tempDirectory, "Jackett", "JackettUpdater.exe"); private string GetCurrentVersion() { var assembly = Assembly.GetExecutingAssembly(); var fvi = FileVersionInfo.GetVersionInfo(assembly.Location); return fvi.ProductVersion; } private WebRequest SetDownloadHeaders(WebRequest req) { req.Headers = new Dictionary { { "Accept", "application/octet-stream" } }; return req; } public void CleanupTempDir() { var tempDir = Path.GetTempPath(); if (!Directory.Exists(tempDir)) { logger.Error("Temp dir doesn't exist: " + tempDir); return; } try { var d = new DirectoryInfo(tempDir); foreach (var dir in d.GetDirectories("JackettUpdate-*")) { try { logger.Info("Deleting JackettUpdate temp files from " + dir.FullName); dir.Delete(true); } catch (Exception e) { logger.Error("Error while deleting temp files from " + dir.FullName); logger.Error(e); } } } catch (Exception e) { logger.Error("Unexpected error while deleting temp files from " + tempDir); logger.Error(e); } } public void CheckUpdaterLock() { // check .lock file to detect errors in the update process var lockFilePath = Path.Combine(Path.GetDirectoryName(ExePath()), ".lock"); if (File.Exists(lockFilePath)) { logger.Error("An error occurred during the last update. If this error occurs again, you need to reinstall " + "Jackett following the documentation. If the problem continues after reinstalling, " + "report the issue and attach the Jackett and Updater logs."); File.Delete(lockFilePath); } } private async Task DownloadRelease(List assets, bool isWindows, string version) { var variants = new Variants(); var artifactFileName = variants.GetArtifactFileName(variant); var targetAsset = assets.FirstOrDefault(a => a.Browser_download_url.EndsWith(artifactFileName, StringComparison.OrdinalIgnoreCase) && artifactFileName.Length > 0); if (targetAsset == null) { logger.Error("Failed to find asset to download!"); return null; } var url = targetAsset.Browser_download_url; var data = await client.GetBytes(SetDownloadHeaders(new WebRequest() { Url = url, EmulateBrowser = true, Type = RequestType.GET })); while (data.IsRedirect) { data = await client.GetBytes(new WebRequest() { Url = data.RedirectingTo, EmulateBrowser = true, Type = RequestType.GET }); } var tempDir = Path.Combine(Path.GetTempPath(), "JackettUpdate-" + version + "-" + DateTime.Now.Ticks); if (Directory.Exists(tempDir)) { Directory.Delete(tempDir, true); } Directory.CreateDirectory(tempDir); if (isWindows) { var zipPath = Path.Combine(tempDir, "Update.zip"); File.WriteAllBytes(zipPath, data.Content); var fastZip = new FastZip(); fastZip.ExtractZip(zipPath, tempDir, null); } else { var gzPath = Path.Combine(tempDir, "Update.tar.gz"); File.WriteAllBytes(gzPath, data.Content); Stream inStream = File.OpenRead(gzPath); Stream gzipStream = new GZipInputStream(inStream); var tarArchive = TarArchive.CreateInputTarArchive(gzipStream); tarArchive.ExtractContents(tempDir); tarArchive.Close(); gzipStream.Close(); inStream.Close(); if (variant == Variants.JackettVariant.CoreMacOs || variant == Variants.JackettVariant.CoreLinuxAmdx64 || variant == Variants.JackettVariant.CoreLinuxArm32 || variant == Variants.JackettVariant.CoreLinuxArm64 || variant == Variants.JackettVariant.Mono) { //Calling the file permission service to limit usage to netcoreapp. The Mono.Posix.NETStandard library causes issues outside of .NET Core //https://github.com/xamarin/XamarinComponents/issues/282 // When the files get extracted, the execute permission for jackett and JackettUpdater don't get carried across var jackettPath = tempDir + "/Jackett/jackett"; filePermissionService.MakeFileExecutable(jackettPath); var jackettUpdaterPath = tempDir + "/Jackett/JackettUpdater"; filePermissionService.MakeFileExecutable(jackettUpdaterPath); if (variant == Variants.JackettVariant.CoreMacOs) { filePermissionService.MakeFileExecutable(tempDir + "/Jackett/install_service_macos"); filePermissionService.MakeFileExecutable(tempDir + "/Jackett/uninstall_jackett_macos"); } else if (variant == Variants.JackettVariant.Mono) { var systemdPath = tempDir + "/Jackett/install_service_systemd_mono.sh"; filePermissionService.MakeFileExecutable(systemdPath); } else { var systemdPath = tempDir + "/Jackett/install_service_systemd.sh"; filePermissionService.MakeFileExecutable(systemdPath); var launcherPath = tempDir + "/Jackett/jackett_launcher.sh"; filePermissionService.MakeFileExecutable(launcherPath); } } } return tempDir; } private void StartUpdate(string updaterExePath, string installLocation, bool isWindows, bool noRestart, bool trayIsRunning) { var appType = "Console"; if (isWindows && windowsService.ServiceExists() && windowsService.ServiceRunning()) { appType = "WindowsService"; } var exe = Path.GetFileName(ExePath()); var args = string.Join(" ", Environment.GetCommandLineArgs().Skip(1).Select(a => a.Contains(" ") ? "\"" + a + "\"" : a)).Replace("\"", "\\\""); var startInfo = new ProcessStartInfo { UseShellExecute = false, CreateNoWindow = true }; // Note: add a leading space to the --Args argument to avoid parsing as arguments if (variant == Variants.JackettVariant.Mono) { // Wrap mono args = exe + " " + args; startInfo.Arguments = $"{Path.Combine(updaterExePath)} --Path \"{installLocation}\" --Type \"{appType}\" --Args \" {args}\""; startInfo.FileName = "mono"; } else { startInfo.Arguments = $"--Path \"{installLocation}\" --Type \"{appType}\" --Args \" {args}\""; startInfo.FileName = Path.Combine(updaterExePath); } try { var pid = Process.GetCurrentProcess().Id; startInfo.Arguments += $" --KillPids \"{pid}\""; } catch (Exception e) { logger.Error("Unexpected error while retriving the PID"); logger.Error(e); } if (noRestart) { startInfo.Arguments += " --NoRestart"; } if (trayIsRunning && appType == "Console") { startInfo.Arguments += " --StartTray"; } // create .lock file to detect errors in the update process var lockFilePath = Path.Combine(installLocation, ".lock"); if (!File.Exists(lockFilePath)) File.Create(lockFilePath).Dispose(); logger.Info($"Starting updater: {startInfo.FileName} {startInfo.Arguments}"); var procInfo = Process.Start(startInfo); logger.Info($"Updater started process id: {procInfo.Id}"); if (!noRestart) { if (isWindows) { logger.Info("Signal sent to lock service"); lockService.Signal(); Thread.Sleep(2000); } logger.Info("Exiting Jackett.."); Environment.Exit(0); } } } }