1 // 2 // AddinStore.cs 3 // 4 // Author: 5 // Lluis Sanchez Gual 6 // 7 // Copyright (C) 2007 Novell, Inc (http://www.novell.com) 8 // 9 // Permission is hereby granted, free of charge, to any person obtaining 10 // a copy of this software and associated documentation files (the 11 // "Software"), to deal in the Software without restriction, including 12 // without limitation the rights to use, copy, modify, merge, publish, 13 // distribute, sublicense, and/or sell copies of the Software, and to 14 // permit persons to whom the Software is furnished to do so, subject to 15 // the following conditions: 16 // 17 // The above copyright notice and this permission notice shall be 18 // included in all copies or substantial portions of the Software. 19 // 20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 // 28 29 using System; 30 using System.Collections; 31 using System.Collections.Specialized; 32 using System.IO; 33 using System.Xml; 34 using System.Xml.Serialization; 35 using System.Reflection; 36 using System.Diagnostics; 37 using System.Net; 38 using System.Runtime.Serialization; 39 using System.Runtime.Serialization.Formatters.Binary; 40 41 using ICSharpCode.SharpZipLib.Zip; 42 using Mono.Addins; 43 using Mono.Addins.Setup.ProgressMonitoring; 44 using Mono.Addins.Description; 45 using Mono.Addins.Serialization; 46 using System.Collections.Generic; 47 using System.Linq; 48 using System.Threading; 49 50 namespace Mono.Addins.Setup 51 { 52 internal class AddinStore 53 { 54 SetupService service; 55 AddinStore(SetupService service)56 public AddinStore (SetupService service) 57 { 58 this.service = service; 59 } 60 ResetCachedData()61 internal void ResetCachedData () 62 { 63 } 64 65 public AddinRegistry Registry { 66 get { return service.Registry; } 67 } 68 Install(IProgressStatus statusMonitor, params string[] files)69 public bool Install (IProgressStatus statusMonitor, params string[] files) 70 { 71 Package[] packages = new Package [files.Length]; 72 for (int n=0; n<files.Length; n++) 73 packages [n] = AddinPackage.FromFile (files [n]); 74 75 return Install (statusMonitor, packages); 76 } 77 Install(IProgressStatus statusMonitor, params AddinRepositoryEntry[] addins)78 public bool Install (IProgressStatus statusMonitor, params AddinRepositoryEntry[] addins) 79 { 80 Package[] packages = new Package [addins.Length]; 81 for (int n=0; n<addins.Length; n++) 82 packages [n] = AddinPackage.FromRepository (addins [n]); 83 84 return Install (statusMonitor, packages); 85 } 86 Install(IProgressStatus monitor, params Package[] packages)87 internal bool Install (IProgressStatus monitor, params Package[] packages) 88 { 89 PackageCollection packs = new PackageCollection (); 90 packs.AddRange (packages); 91 return Install (monitor, packs); 92 } 93 Install(IProgressStatus statusMonitor, PackageCollection packs)94 internal bool Install (IProgressStatus statusMonitor, PackageCollection packs) 95 { 96 // Make sure the registry is up to date 97 service.Registry.Update (statusMonitor); 98 99 IProgressMonitor monitor = ProgressStatusMonitor.GetProgressMonitor (statusMonitor); 100 101 PackageCollection toUninstall; 102 DependencyCollection unresolved; 103 if (!ResolveDependencies (monitor, packs, out toUninstall, out unresolved)) { 104 monitor.ReportError ("Not all dependencies could be resolved.", null); 105 return false; 106 } 107 108 ArrayList prepared = new ArrayList (); 109 ArrayList uninstallPrepared = new ArrayList (); 110 bool rollback = false; 111 112 monitor.BeginTask ("Installing add-ins...", 100); 113 114 // Prepare install 115 116 monitor.BeginStepTask ("Initializing installation", toUninstall.Count + packs.Count + 1, 75); 117 118 foreach (Package mpack in toUninstall) { 119 try { 120 mpack.PrepareUninstall (monitor, this); 121 uninstallPrepared.Add (mpack); 122 if (monitor.IsCancelRequested) 123 throw new InstallException ("Installation cancelled."); 124 monitor.Step (1); 125 } catch (Exception ex) { 126 ReportException (monitor, ex); 127 rollback = true; 128 break; 129 } 130 } 131 132 monitor.Step (1); 133 134 foreach (Package mpack in packs) { 135 try { 136 mpack.PrepareInstall (monitor, this); 137 if (monitor.IsCancelRequested) 138 throw new InstallException ("Installation cancelled."); 139 prepared.Add (mpack); 140 monitor.Step (1); 141 } catch (Exception ex) { 142 ReportException (monitor, ex); 143 rollback = true; 144 break; 145 } 146 } 147 148 monitor.EndTask (); 149 150 monitor.BeginStepTask ("Installing", toUninstall.Count + packs.Count + 1, 20); 151 152 // Commit install 153 154 if (!rollback) { 155 foreach (Package mpack in toUninstall) { 156 try { 157 mpack.CommitUninstall (monitor, this); 158 if (monitor.IsCancelRequested) 159 throw new InstallException ("Installation cancelled."); 160 monitor.Step (1); 161 } catch (Exception ex) { 162 ReportException (monitor, ex); 163 rollback = true; 164 break; 165 } 166 } 167 } 168 169 monitor.Step (1); 170 171 if (!rollback) { 172 foreach (Package mpack in packs) { 173 try { 174 mpack.CommitInstall (monitor, this); 175 if (monitor.IsCancelRequested) 176 throw new InstallException ("Installation cancelled."); 177 monitor.Step (1); 178 } catch (Exception ex) { 179 ReportException (monitor, ex); 180 rollback = true; 181 break; 182 } 183 } 184 } 185 186 monitor.EndTask (); 187 188 // Rollback if failed 189 190 if (monitor.IsCancelRequested) 191 monitor = new NullProgressMonitor (); 192 193 if (rollback) { 194 monitor.BeginStepTask ("Finishing installation", (prepared.Count + uninstallPrepared.Count)*2 + 1, 5); 195 196 foreach (Package mpack in prepared) { 197 try { 198 mpack.RollbackInstall (monitor, this); 199 monitor.Step (1); 200 } catch (Exception ex) { 201 ReportException (monitor, ex); 202 } 203 } 204 205 foreach (Package mpack in uninstallPrepared) { 206 try { 207 mpack.RollbackUninstall (monitor, this); 208 monitor.Step (1); 209 } catch (Exception ex) { 210 ReportException (monitor, ex); 211 } 212 } 213 } else 214 monitor.BeginStepTask ("Finishing installation", prepared.Count + uninstallPrepared.Count + 1, 5); 215 216 // Cleanup 217 218 foreach (Package mpack in prepared) { 219 try { 220 mpack.EndInstall (monitor, this); 221 monitor.Step (1); 222 } catch (Exception ex) { 223 monitor.Log.WriteLine (ex); 224 } 225 } 226 227 monitor.Step (1); 228 229 foreach (Package mpack in uninstallPrepared) { 230 try { 231 mpack.EndUninstall (monitor, this); 232 monitor.Step (1); 233 } catch (Exception ex) { 234 monitor.Log.WriteLine (ex); 235 } 236 } 237 238 // Update the extension maps 239 service.Registry.Update (statusMonitor); 240 241 monitor.EndTask (); 242 243 monitor.EndTask (); 244 245 service.SaveConfiguration (); 246 ResetCachedData (); 247 248 return !rollback; 249 } 250 ReportException(IProgressMonitor statusMonitor, Exception ex)251 void ReportException (IProgressMonitor statusMonitor, Exception ex) 252 { 253 if (ex is InstallException) 254 statusMonitor.ReportError (ex.Message, null); 255 else 256 statusMonitor.ReportError (null, ex); 257 } 258 Uninstall(IProgressStatus statusMonitor, string id)259 public void Uninstall (IProgressStatus statusMonitor, string id) 260 { 261 Uninstall (statusMonitor, new string[] { id }); 262 } 263 Uninstall(IProgressStatus statusMonitor, IEnumerable<string> ids)264 public void Uninstall (IProgressStatus statusMonitor, IEnumerable<string> ids) 265 { 266 IProgressMonitor monitor = ProgressStatusMonitor.GetProgressMonitor (statusMonitor); 267 monitor.BeginTask ("Uninstalling add-ins", ids.Count ()); 268 269 foreach (string id in ids) { 270 bool rollback = false; 271 ArrayList toUninstall = new ArrayList (); 272 ArrayList uninstallPrepared = new ArrayList (); 273 274 Addin ia = service.Registry.GetAddin (id); 275 if (ia == null) 276 throw new InstallException ("The add-in '" + id + "' is not installed."); 277 278 toUninstall.Add (AddinPackage.FromInstalledAddin (ia)); 279 280 Addin[] deps = GetDependentAddins (id, true); 281 foreach (Addin dep in deps) 282 toUninstall.Add (AddinPackage.FromInstalledAddin (dep)); 283 284 monitor.BeginTask ("Deleting files", toUninstall.Count*2 + uninstallPrepared.Count + 1); 285 286 // Prepare install 287 288 foreach (Package mpack in toUninstall) { 289 try { 290 mpack.PrepareUninstall (monitor, this); 291 monitor.Step (1); 292 uninstallPrepared.Add (mpack); 293 } catch (Exception ex) { 294 ReportException (monitor, ex); 295 rollback = true; 296 break; 297 } 298 } 299 300 // Commit install 301 302 if (!rollback) { 303 foreach (Package mpack in toUninstall) { 304 try { 305 mpack.CommitUninstall (monitor, this); 306 monitor.Step (1); 307 } catch (Exception ex) { 308 ReportException (monitor, ex); 309 rollback = true; 310 break; 311 } 312 } 313 } 314 315 // Rollback if failed 316 317 if (rollback) { 318 monitor.BeginTask ("Rolling back uninstall", uninstallPrepared.Count); 319 foreach (Package mpack in uninstallPrepared) { 320 try { 321 mpack.RollbackUninstall (monitor, this); 322 } catch (Exception ex) { 323 ReportException (monitor, ex); 324 } 325 } 326 monitor.EndTask (); 327 } 328 monitor.Step (1); 329 330 // Cleanup 331 332 foreach (Package mpack in uninstallPrepared) { 333 try { 334 mpack.EndUninstall (monitor, this); 335 monitor.Step (1); 336 } catch (Exception ex) { 337 monitor.Log.WriteLine (ex); 338 } 339 } 340 341 monitor.EndTask (); 342 monitor.Step (1); 343 } 344 345 // Update the extension maps 346 service.Registry.Update (statusMonitor); 347 348 monitor.EndTask (); 349 350 service.SaveConfiguration (); 351 ResetCachedData (); 352 } 353 GetDependentAddins(string id, bool recursive)354 public Addin[] GetDependentAddins (string id, bool recursive) 355 { 356 ArrayList list = new ArrayList (); 357 FindDependentAddins (list, id, recursive); 358 return (Addin[]) list.ToArray (typeof (Addin)); 359 } 360 FindDependentAddins(ArrayList list, string id, bool recursive)361 void FindDependentAddins (ArrayList list, string id, bool recursive) 362 { 363 foreach (Addin iaddin in service.Registry.GetAddins ()) { 364 if (list.Contains (iaddin)) 365 continue; 366 foreach (Dependency dep in iaddin.Description.MainModule.Dependencies) { 367 AddinDependency adep = dep as AddinDependency; 368 if (adep != null && adep.AddinId == id) { 369 list.Add (iaddin); 370 if (recursive) 371 FindDependentAddins (list, iaddin.Id, true); 372 } 373 } 374 } 375 } 376 ResolveDependencies(IProgressStatus statusMonitor, AddinRepositoryEntry[] addins, out PackageCollection resolved, out PackageCollection toUninstall, out DependencyCollection unresolved)377 public bool ResolveDependencies (IProgressStatus statusMonitor, AddinRepositoryEntry[] addins, out PackageCollection resolved, out PackageCollection toUninstall, out DependencyCollection unresolved) 378 { 379 resolved = new PackageCollection (); 380 for (int n=0; n<addins.Length; n++) 381 resolved.Add (AddinPackage.FromRepository (addins [n])); 382 return ResolveDependencies (statusMonitor, resolved, out toUninstall, out unresolved); 383 } 384 ResolveDependencies(IProgressStatus statusMonitor, PackageCollection packages, out PackageCollection toUninstall, out DependencyCollection unresolved)385 public bool ResolveDependencies (IProgressStatus statusMonitor, PackageCollection packages, out PackageCollection toUninstall, out DependencyCollection unresolved) 386 { 387 IProgressMonitor monitor = ProgressStatusMonitor.GetProgressMonitor (statusMonitor); 388 return ResolveDependencies (monitor, packages, out toUninstall, out unresolved); 389 } 390 ResolveDependencies(IProgressMonitor monitor, PackageCollection packages, out PackageCollection toUninstall, out DependencyCollection unresolved)391 internal bool ResolveDependencies (IProgressMonitor monitor, PackageCollection packages, out PackageCollection toUninstall, out DependencyCollection unresolved) 392 { 393 PackageCollection requested = new PackageCollection(); 394 requested.AddRange (packages); 395 396 unresolved = new DependencyCollection (); 397 toUninstall = new PackageCollection (); 398 PackageCollection installedRequired = new PackageCollection (); 399 400 for (int n=0; n<packages.Count; n++) { 401 Package p = packages [n]; 402 p.Resolve (monitor, this, packages, toUninstall, installedRequired, unresolved); 403 } 404 405 if (unresolved.Count != 0) { 406 foreach (Dependency dep in unresolved) 407 monitor.ReportError (string.Format ("The package '{0}' could not be found in any repository", dep.Name), null); 408 return false; 409 } 410 411 // Check that we are not uninstalling packages that are required 412 // by packages being installed. 413 414 foreach (Package p in installedRequired) { 415 if (toUninstall.Contains (p)) { 416 // Only accept to uninstall this package if we are 417 // going to install a newer version. 418 bool foundUpgrade = false; 419 foreach (Package tbi in packages) 420 if (tbi.Equals (p) || tbi.IsUpgradeOf (p)) { 421 foundUpgrade = true; 422 break; 423 } 424 if (!foundUpgrade) 425 return false; 426 } 427 } 428 429 // Check that we are not trying to uninstall from a directory from 430 // which we don't have write permissions 431 432 foreach (Package p in toUninstall) { 433 AddinPackage ap = p as AddinPackage; 434 if (ap != null) { 435 Addin ia = service.Registry.GetAddin (ap.Addin.Id); 436 if (File.Exists (ia.AddinFile) && !HasWriteAccess (ia.AddinFile) && IsUserAddin (ia.AddinFile)) { 437 monitor.ReportError (GetUninstallErrorNoRoot (ap.Addin), null); 438 return false; 439 } 440 } 441 } 442 443 // Check that we are not installing two versions of the same addin 444 445 PackageCollection resolved = new PackageCollection(); 446 resolved.AddRange (packages); 447 448 bool error = false; 449 450 for (int n=0; n<packages.Count; n++) { 451 AddinPackage ap = packages [n] as AddinPackage; 452 if (ap == null) continue; 453 454 for (int k=n+1; k<packages.Count; k++) { 455 AddinPackage otherap = packages [k] as AddinPackage; 456 if (otherap == null) continue; 457 458 if (ap.Addin.Id == otherap.Addin.Id) { 459 if (ap.IsUpgradeOf (otherap)) { 460 if (requested.Contains (otherap)) { 461 monitor.ReportError ("Can't install two versions of the same add-in: '" + ap.Addin.Name + "'.", null); 462 error = true; 463 } else { 464 packages.RemoveAt (k); 465 } 466 } else if (otherap.IsUpgradeOf (ap)) { 467 if (requested.Contains (ap)) { 468 monitor.ReportError ("Can't install two versions of the same add-in: '" + ap.Addin.Name + "'.", null); 469 error = true; 470 } else { 471 packages.RemoveAt (n); 472 n--; 473 } 474 } else { 475 error = true; 476 monitor.ReportError ("Can't install two versions of the same add-in: '" + ap.Addin.Name + "'.", null); 477 } 478 break; 479 } 480 } 481 } 482 483 // Don't allow installing add-ins which are scheduled for uninstall 484 485 foreach (Package p in packages) { 486 AddinPackage ap = p as AddinPackage; 487 if (ap != null && Registry.IsRegisteredForUninstall (ap.Addin.Id)) { 488 error = true; 489 monitor.ReportError ("The addin " + ap.Addin.Name + " v" + ap.Addin.Version + " is scheduled for uninstallation. Please restart the application before trying to re-install it.", null); 490 } 491 } 492 493 return !error; 494 } 495 ResolveDependency(IProgressMonitor monitor, Dependency dep, AddinPackage parentPackage, PackageCollection toInstall, PackageCollection toUninstall, PackageCollection installedRequired, DependencyCollection unresolved)496 internal void ResolveDependency (IProgressMonitor monitor, Dependency dep, AddinPackage parentPackage, PackageCollection toInstall, PackageCollection toUninstall, PackageCollection installedRequired, DependencyCollection unresolved) 497 { 498 AddinDependency adep = dep as AddinDependency; 499 if (adep == null) 500 return; 501 502 string nsid = Addin.GetFullId (parentPackage.Addin.Namespace, adep.AddinId, null); 503 504 foreach (Package p in toInstall) { 505 AddinPackage ap = p as AddinPackage; 506 if (ap != null) { 507 if (Addin.GetIdName (ap.Addin.Id) == nsid && ((AddinInfo)ap.Addin).SupportsVersion (adep.Version)) 508 return; 509 } 510 } 511 512 ArrayList addins = new ArrayList (); 513 addins.AddRange (service.Registry.GetAddins ()); 514 addins.AddRange (service.Registry.GetAddinRoots ()); 515 516 foreach (Addin addin in addins) { 517 if (Addin.GetIdName (addin.Id) == nsid && addin.SupportsVersion (adep.Version)) { 518 AddinPackage p = AddinPackage.FromInstalledAddin (addin); 519 if (!installedRequired.Contains (p)) 520 installedRequired.Add (p); 521 return; 522 } 523 } 524 525 AddinRepositoryEntry[] avaddins = service.Repositories.GetAvailableAddins (); 526 foreach (PackageRepositoryEntry avAddin in avaddins) { 527 if (Addin.GetIdName (avAddin.Addin.Id) == nsid && ((AddinInfo)avAddin.Addin).SupportsVersion (adep.Version)) { 528 toInstall.Add (AddinPackage.FromRepository (avAddin)); 529 return; 530 } 531 } 532 unresolved.Add (adep); 533 } 534 GetAddinDirectory(AddinInfo info)535 internal string GetAddinDirectory (AddinInfo info) 536 { 537 return Path.Combine (service.InstallDirectory, info.Id.Replace (',','.')); 538 } 539 RegisterAddin(IProgressMonitor monitor, AddinInfo info, string sourceDir)540 internal void RegisterAddin (IProgressMonitor monitor, AddinInfo info, string sourceDir) 541 { 542 monitor.Log.WriteLine ("Installing " + info.Name + " v" + info.Version); 543 string addinDir = GetAddinDirectory (info); 544 if (!Directory.Exists (addinDir)) 545 Directory.CreateDirectory (addinDir); 546 CopyDirectory (sourceDir, addinDir); 547 548 ResetCachedData (); 549 } 550 CopyDirectory(string src, string dest)551 void CopyDirectory (string src, string dest) 552 { 553 CopyDirectory (src, dest, ""); 554 } 555 CopyDirectory(string src, string dest, string subdir)556 void CopyDirectory (string src, string dest, string subdir) 557 { 558 string destDir = Path.Combine (dest, subdir); 559 560 if (!Directory.Exists (destDir)) 561 Directory.CreateDirectory (destDir); 562 563 foreach (string file in Directory.GetFiles (src)) { 564 if (Path.GetFileName (file) != "addin.info") 565 File.Copy (file, Path.Combine (destDir, Path.GetFileName (file)), true); 566 } 567 568 foreach (string dir in Directory.GetDirectories (src)) 569 CopyDirectory (dir, dest, Path.Combine (subdir, Path.GetFileName (dir))); 570 } 571 DownloadObject(IProgressMonitor monitor, string url, Type type)572 internal object DownloadObject (IProgressMonitor monitor, string url, Type type) 573 { 574 string file = null; 575 try { 576 file = DownloadFile (monitor, url); 577 return ReadObject (file, type); 578 } finally { 579 if (file != null) 580 File.Delete (file); 581 } 582 } 583 GetSerializer(Type type)584 static XmlSerializer GetSerializer (Type type) 585 { 586 if (type == typeof(AddinSystemConfiguration)) 587 return new AddinSystemConfigurationSerializer (); 588 else if (type == typeof(Repository)) 589 return new RepositorySerializer (); 590 else 591 return new XmlSerializer (type); 592 } 593 ReadObject(string file, Type type)594 internal static object ReadObject (string file, Type type) 595 { 596 if (!File.Exists (file)) 597 return null; 598 599 StreamReader r = new StreamReader (file); 600 try { 601 XmlSerializer ser = GetSerializer (type); 602 return ser.Deserialize (r); 603 } catch { 604 return null; 605 } finally { 606 r.Close (); 607 } 608 } 609 WriteObject(string file, object obj)610 internal static void WriteObject (string file, object obj) 611 { 612 string dir = Path.GetDirectoryName (file); 613 if (!Directory.Exists (dir)) 614 Directory.CreateDirectory (dir); 615 StreamWriter s = new StreamWriter (file); 616 try { 617 XmlSerializer ser = GetSerializer (obj.GetType()); 618 ser.Serialize (s, obj); 619 s.Close (); 620 } catch { 621 s.Close (); 622 if (File.Exists (file)) 623 File.Delete (file); 624 throw; 625 } 626 } 627 DownloadFile(IProgressMonitor monitor, string url)628 internal string DownloadFile (IProgressMonitor monitor, string url) 629 { 630 if (url.StartsWith ("file://", StringComparison.Ordinal)) { 631 string tmpfile = Path.GetTempFileName (); 632 string path = new Uri (url).LocalPath; 633 File.Delete (tmpfile); 634 File.Copy (path, tmpfile); 635 return tmpfile; 636 } 637 638 string file = null; 639 FileStream fs = null; 640 Stream s = null; 641 642 try { 643 monitor.BeginTask ("Requesting " + url, 2); 644 var resp = WebRequestHelper.GetResponse ( 645 () => (HttpWebRequest)WebRequest.Create (url), 646 r => r.Headers ["Pragma"] = "no-cache" 647 ); 648 monitor.Step (1); 649 monitor.BeginTask ("Downloading " + url, (int) resp.ContentLength); 650 651 file = Path.GetTempFileName (); 652 fs = new FileStream (file, FileMode.Create, FileAccess.Write); 653 s = resp.GetResponseStream (); 654 byte[] buffer = new byte [4096]; 655 656 int n; 657 while ((n = s.Read (buffer, 0, buffer.Length)) != 0) { 658 monitor.Step (n); 659 fs.Write (buffer, 0, n); 660 if (monitor.IsCancelRequested) 661 throw new InstallException ("Installation cancelled."); 662 } 663 fs.Close (); 664 s.Close (); 665 return file; 666 } catch { 667 if (fs != null) 668 fs.Close (); 669 if (s != null) 670 s.Close (); 671 if (file != null) 672 File.Delete (file); 673 throw; 674 } finally { 675 monitor.EndTask (); 676 monitor.EndTask (); 677 } 678 } 679 HasWriteAccess(string file)680 internal bool HasWriteAccess (string file) 681 { 682 FileInfo f = new FileInfo (file); 683 return !f.Exists || !f.IsReadOnly; 684 } 685 IsUserAddin(string addinFile)686 internal bool IsUserAddin (string addinFile) 687 { 688 string installPath = service.InstallDirectory; 689 if (installPath [installPath.Length - 1] != Path.DirectorySeparatorChar) 690 installPath += Path.DirectorySeparatorChar; 691 return Path.GetFullPath (addinFile).StartsWith (installPath); 692 } 693 GetUninstallErrorNoRoot(AddinHeader ainfo)694 internal static string GetUninstallErrorNoRoot (AddinHeader ainfo) 695 { 696 return string.Format ("The add-in '{0} v{1}' can't be uninstalled with the current user permissions.", ainfo.Name, ainfo.Version); 697 } 698 } 699 } 700