public Task CreateShortcutsForExecutableAsync(SnapOsShortcutDescription shortcutDescription, ILog logger = null, CancellationToken cancellationToken = default) { if (shortcutDescription == null) { throw new ArgumentNullException(nameof(shortcutDescription)); } var baseDirectory = Filesystem.PathGetDirectoryName(shortcutDescription.ExeAbsolutePath); var exeName = Filesystem.PathGetFileName(shortcutDescription.ExeAbsolutePath); var packageTitle = shortcutDescription.NuspecReader.GetTitle(); var authors = shortcutDescription.NuspecReader.GetAuthors(); var packageId = shortcutDescription.NuspecReader.GetIdentity().Id; var packageVersion = shortcutDescription.NuspecReader.GetIdentity().Version; var packageDescription = shortcutDescription.NuspecReader.GetDescription(); string LinkTargetForVersionInfo(SnapShortcutLocation location, FileVersionInfo versionInfo) { var possibleProductNames = new[] { versionInfo.ProductName, packageTitle, versionInfo.FileDescription, Filesystem.PathGetFileNameWithoutExtension(versionInfo.FileName) }; var possibleCompanyNames = new[] { versionInfo.CompanyName, authors ?? packageId }; var productName = possibleCompanyNames.First(x => !string.IsNullOrWhiteSpace(x)); var packageName = possibleProductNames.First(x => !string.IsNullOrWhiteSpace(x)); return(GetLinkTarget(location, packageName, productName)); } string GetLinkTarget(SnapShortcutLocation location, string title, string applicationName, bool createDirectoryIfNecessary = true) { var targetDirectory = location switch { SnapShortcutLocation.Desktop => SpecialFolders.DesktopDirectory, SnapShortcutLocation.StartMenu => Filesystem.PathCombine(SpecialFolders.StartMenu, "Programs", applicationName), SnapShortcutLocation.Startup => SpecialFolders.StartupDirectory, _ => throw new ArgumentOutOfRangeException(nameof(location), location, null) }; if (createDirectoryIfNecessary) { Filesystem.DirectoryCreateIfNotExists(targetDirectory); } return(Filesystem.PathCombine(targetDirectory, title + ".lnk")); } logger?.Info($"About to create shortcuts for {exeName}, base directory {baseDirectory}"); var fileVerInfo = FileVersionInfo.GetVersionInfo(shortcutDescription.ExeAbsolutePath); foreach (var flag in (SnapShortcutLocation[])Enum.GetValues(typeof(SnapShortcutLocation))) { if (!shortcutDescription.ShortcutLocations.HasFlag(flag)) { continue; } var file = LinkTargetForVersionInfo(flag, fileVerInfo); var fileExists = Filesystem.FileExists(file); // NB: If we've already installed the app, but the shortcut // is no longer there, we have to assume that the user didn't // want it there and explicitly deleted it, so we shouldn't // annoy them by recreating it. if (!fileExists && shortcutDescription.UpdateOnly) { logger?.Warn($"Wanted to update shortcut {file} but it appears user deleted it"); continue; } logger?.Info($"Creating shortcut for {exeName} => {file}"); ShellLink shellLink; logger?.ErrorIfThrows(() => SnapUtility.Retry(() => { Filesystem.FileDelete(file); shellLink = new ShellLink { Target = shortcutDescription.ExeAbsolutePath, IconPath = shortcutDescription.IconAbsolutePath ?? shortcutDescription.ExeAbsolutePath, IconIndex = 0, WorkingDirectory = Filesystem.PathGetDirectoryName(shortcutDescription.ExeAbsolutePath), Description = packageDescription }; if (!string.IsNullOrWhiteSpace(shortcutDescription.ExeProgramArguments)) { shellLink.Arguments += $" -a \"{shortcutDescription.ExeProgramArguments}\""; } var appUserModelId = $"com.snap.{packageId.Replace(" ", "")}.{exeName.Replace(".exe", string.Empty).Replace(" ", string.Empty)}"; var toastActivatorClsid = SnapUtility.CreateGuidFromHash(appUserModelId).ToString(); shellLink.SetAppUserModelId(appUserModelId); shellLink.SetToastActivatorCLSID(toastActivatorClsid); logger.Info($"Saving shortcut: {file}. " + $"Target: {shellLink.Target}. " + $"Working directory: {shellLink.WorkingDirectory}. " + $"Arguments: {shellLink.Arguments}. " + $"ToastActivatorCSLID: {toastActivatorClsid}."); if (_isUnitTest == false) { shellLink.Save(file); } }, 4), $"Can't write shortcut: {file}"); } FixPinnedExecutables(baseDirectory, packageVersion, logger: logger); return(Task.CompletedTask); } void FixPinnedExecutables(string baseDirectory, SemanticVersion newCurrentVersion, bool removeAll = false, ILog logger = null) { if (Environment.OSVersion.Version < new Version(6, 1)) { logger?.Warn($"fixPinnedExecutables: Found OS Version '{Environment.OSVersion.VersionString}', exiting"); return; } var newCurrentFolder = "app-" + newCurrentVersion; var newAppPath = Filesystem.PathCombine(baseDirectory, newCurrentFolder); var taskbarPath = Filesystem.PathCombine(SpecialFolders.ApplicationData, "Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar"); if (!Filesystem.DirectoryExists(taskbarPath)) { logger?.Info("fixPinnedExecutables: PinnedExecutables directory doesn't exist, skipping"); return; } var resolveLink = new Func <FileInfo, ShellLink>(file => { try { logger?.Debug("Examining Pin: " + file); return(new ShellLink(file.FullName)); } catch (Exception ex) { var message = $"File '{file.FullName}' could not be converted into a valid ShellLink"; logger?.WarnException(message, ex); return(null); } }); var shellLinks = new DirectoryInfo(taskbarPath).GetFiles("*.lnk").Select(resolveLink).ToArray(); foreach (var shortcut in shellLinks) { try { if (shortcut == null) { continue; } if (string.IsNullOrWhiteSpace(shortcut.Target)) { continue; } if (!shortcut.Target.StartsWith(baseDirectory, StringComparison.OrdinalIgnoreCase)) { continue; } if (removeAll) { Filesystem.FileDeleteWithRetries(shortcut.ShortCutFile); } else { UpdateShellLink(baseDirectory, shortcut, newAppPath, logger); } } catch (Exception ex) { var message = $"fixPinnedExecutables: shortcut failed: {shortcut?.Target}"; logger?.ErrorException(message, ex); } } } void UpdateShellLink([JetBrains.Annotations.NotNull] string baseDirectory, [JetBrains.Annotations.NotNull] ShellLink shortcut, [JetBrains.Annotations.NotNull] string newAppPath, ILog logger = null)
public Task CreateShortcutsForExecutableAsync(SnapOsShortcutDescription shortcutDescription, ILog logger = null, CancellationToken cancellationToken = default) { if (shortcutDescription == null) { throw new ArgumentNullException(nameof(shortcutDescription)); } var baseDirectory = Filesystem.PathGetDirectoryName(shortcutDescription.ExeAbsolutePath); var exeName = Filesystem.PathGetFileName(shortcutDescription.ExeAbsolutePath); var packageTitle = shortcutDescription.NuspecReader.GetTitle(); var authors = shortcutDescription.NuspecReader.GetAuthors(); var packageId = shortcutDescription.NuspecReader.GetIdentity().Id; var packageVersion = shortcutDescription.NuspecReader.GetIdentity().Version; var packageDescription = shortcutDescription.NuspecReader.GetDescription(); string LinkTargetForVersionInfo(SnapShortcutLocation location, FileVersionInfo versionInfo) { var possibleProductNames = new[] { versionInfo.ProductName, packageTitle, versionInfo.FileDescription, Filesystem.PathGetFileNameWithoutExtension(versionInfo.FileName) }; var possibleCompanyNames = new[] { versionInfo.CompanyName, authors ?? packageId }; var productName = possibleCompanyNames.First(x => !string.IsNullOrWhiteSpace(x)); var packageName = possibleProductNames.First(x => !string.IsNullOrWhiteSpace(x)); return(GetLinkTarget(location, packageName, productName)); } string GetLinkTarget(SnapShortcutLocation location, string title, string applicationName, bool createDirectoryIfNecessary = true) { string targetDirectory; switch (location) { case SnapShortcutLocation.Desktop: targetDirectory = SpecialFolders.DesktopDirectory; break; case SnapShortcutLocation.StartMenu: targetDirectory = Filesystem.PathCombine(SpecialFolders.StartMenu, "Programs", applicationName); break; case SnapShortcutLocation.Startup: targetDirectory = SpecialFolders.StartupDirectory; break; default: throw new ArgumentOutOfRangeException(nameof(location), location, null); } if (createDirectoryIfNecessary) { Filesystem.DirectoryCreateIfNotExists(targetDirectory); } return(Filesystem.PathCombine(targetDirectory, title + ".lnk")); } logger?.Info("About to create shortcuts for {0}, base directory {1}", exeName, baseDirectory); var fileVerInfo = FileVersionInfo.GetVersionInfo(shortcutDescription.ExeAbsolutePath); foreach (var flag in (SnapShortcutLocation[])Enum.GetValues(typeof(SnapShortcutLocation))) { if (!shortcutDescription.ShortcutLocations.HasFlag(flag)) { continue; } var file = LinkTargetForVersionInfo(flag, fileVerInfo); var fileExists = Filesystem.FileExists(file); // NB: If we've already installed the app, but the shortcut // is no longer there, we have to assume that the user didn't // want it there and explicitly deleted it, so we shouldn't // annoy them by recreating it. if (!fileExists && shortcutDescription.UpdateOnly) { logger?.Warn("Wanted to update shortcut {0} but it appears user deleted it", file); continue; } logger?.Info("Creating shortcut for {0} => {1}", exeName, file); ShellLink shellLink; logger?.ErrorIfThrows(() => SnapUtility.Retry(() => { Filesystem.FileDelete(file); shellLink = new ShellLink { Target = shortcutDescription.ExeAbsolutePath, IconPath = shortcutDescription.IconAbsolutePath ?? shortcutDescription.ExeAbsolutePath, IconIndex = 0, WorkingDirectory = Filesystem.PathGetDirectoryName(shortcutDescription.ExeAbsolutePath), Description = packageDescription }; if (!string.IsNullOrWhiteSpace(shortcutDescription.ExeProgramArguments)) { shellLink.Arguments += $" -a \"{shortcutDescription.ExeProgramArguments}\""; } var appUserModelId = $"com.snap.{packageId.Replace(" ", "")}.{exeName.Replace(".exe", string.Empty).Replace(" ", string.Empty)}"; var toastActivatorClsid = SnapUtility.CreateGuidFromHash(appUserModelId).ToString(); shellLink.SetAppUserModelId(appUserModelId); shellLink.SetToastActivatorCLSID(toastActivatorClsid); logger.Info($"Saving shortcut: {file}. " + $"Target: {shellLink.Target}. " + $"Working directory: {shellLink.WorkingDirectory}. " + $"Arguments: {shellLink.Arguments}. " + $"ToastActivatorCSLID: {toastActivatorClsid}."); if (_isUnitTest == false) { shellLink.Save(file); } }, 4), $"Can't write shortcut: {file}"); } FixPinnedExecutables(baseDirectory, packageVersion, logger: logger); return(Task.CompletedTask); }